Backed out changeset 496886cb30a5 (bug 1867152) for bc failures on browser_user_input...
[gecko.git] / layout / generic / nsGfxScrollFrame.cpp
blob6dcd6453172e83457174e095350a14ff6dbfc47c
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /* rendering object to wrap rendering objects that should be scrollable */
9 #include "nsGfxScrollFrame.h"
11 #include "ScrollPositionUpdate.h"
12 #include "mozilla/layers/LayersTypes.h"
13 #include "nsIXULRuntime.h"
14 #include "base/compiler_specific.h"
15 #include "DisplayItemClip.h"
16 #include "nsCOMPtr.h"
17 #include "nsIDocumentViewer.h"
18 #include "nsPresContext.h"
19 #include "nsView.h"
20 #include "nsViewportInfo.h"
21 #include "nsContainerFrame.h"
22 #include "nsGkAtoms.h"
23 #include "nsNameSpaceManager.h"
24 #include "mozilla/intl/BidiEmbeddingLevel.h"
25 #include "mozilla/dom/DocumentInlines.h"
26 #include "mozilla/gfx/gfxVars.h"
27 #include "nsFontMetrics.h"
28 #include "mozilla/dom/NodeInfo.h"
29 #include "nsScrollbarFrame.h"
30 #include "nsINode.h"
31 #include "nsIScrollbarMediator.h"
32 #include "nsITextControlFrame.h"
33 #include "nsILayoutHistoryState.h"
34 #include "nsNodeInfoManager.h"
35 #include "nsContentCreatorFunctions.h"
36 #include "nsStyleTransformMatrix.h"
37 #include "mozilla/PresState.h"
38 #include "nsContentUtils.h"
39 #include "nsDisplayList.h"
40 #include "nsHTMLDocument.h"
41 #include "nsLayoutUtils.h"
42 #include "nsBidiPresUtils.h"
43 #include "nsBidiUtils.h"
44 #include "nsDocShell.h"
45 #include "mozilla/ContentEvents.h"
46 #include "mozilla/DisplayPortUtils.h"
47 #include "mozilla/EventDispatcher.h"
48 #include "mozilla/Preferences.h"
49 #include "mozilla/PresShell.h"
50 #include "mozilla/ScopeExit.h"
51 #include "mozilla/ScrollbarPreferences.h"
52 #include "mozilla/ScrollingMetrics.h"
53 #include "mozilla/StaticPrefs_browser.h"
54 #include "mozilla/StaticPrefs_toolkit.h"
55 #include "mozilla/StaticPtr.h"
56 #include "mozilla/SVGOuterSVGFrame.h"
57 #include "mozilla/ViewportUtils.h"
58 #include "mozilla/LookAndFeel.h"
59 #include "mozilla/dom/Element.h"
60 #include "mozilla/dom/Event.h"
61 #include "mozilla/dom/HTMLMarqueeElement.h"
62 #include "mozilla/dom/ScrollTimeline.h"
63 #include "mozilla/dom/BrowserChild.h"
64 #include <stdint.h>
65 #include "mozilla/MathAlgorithms.h"
66 #include "mozilla/Telemetry.h"
67 #include "nsSubDocumentFrame.h"
68 #include "mozilla/Attributes.h"
69 #include "ScrollbarActivity.h"
70 #include "nsRefreshDriver.h"
71 #include "nsStyleConsts.h"
72 #include "nsIScrollPositionListener.h"
73 #include "StickyScrollContainer.h"
74 #include "nsIFrameInlines.h"
75 #include "gfxPlatform.h"
76 #include "mozilla/StaticPrefs_apz.h"
77 #include "mozilla/StaticPrefs_general.h"
78 #include "mozilla/StaticPrefs_layers.h"
79 #include "mozilla/StaticPrefs_layout.h"
80 #include "mozilla/StaticPrefs_mousewheel.h"
81 #include "mozilla/ToString.h"
82 #include "ScrollAnimationPhysics.h"
83 #include "ScrollAnimationBezierPhysics.h"
84 #include "ScrollAnimationMSDPhysics.h"
85 #include "ScrollSnap.h"
86 #include "UnitTransforms.h"
87 #include "nsSliderFrame.h"
88 #include "ViewportFrame.h"
89 #include "mozilla/gfx/gfxVars.h"
90 #include "mozilla/layers/APZCCallbackHelper.h"
91 #include "mozilla/layers/APZPublicUtils.h"
92 #include "mozilla/layers/AxisPhysicsModel.h"
93 #include "mozilla/layers/AxisPhysicsMSDModel.h"
94 #include "mozilla/layers/ScrollingInteractionContext.h"
95 #include "mozilla/layers/ScrollLinkedEffectDetector.h"
96 #include "mozilla/Unused.h"
97 #include "MobileViewportManager.h"
98 #include "VisualViewport.h"
99 #include "WindowRenderer.h"
100 #include <algorithm>
101 #include <cstdlib> // for std::abs(int/long)
102 #include <cmath> // for std::abs(float/double)
103 #include <tuple> // for std::tie
105 static mozilla::LazyLogModule sApzPaintSkipLog("apz.paintskip");
106 #define PAINT_SKIP_LOG(...) \
107 MOZ_LOG(sApzPaintSkipLog, LogLevel::Debug, (__VA_ARGS__))
108 static mozilla::LazyLogModule sScrollRestoreLog("scrollrestore");
109 #define SCROLLRESTORE_LOG(...) \
110 MOZ_LOG(sScrollRestoreLog, LogLevel::Debug, (__VA_ARGS__))
111 static mozilla::LazyLogModule sRootScrollbarsLog("rootscrollbars");
112 #define ROOT_SCROLLBAR_LOG(...) \
113 if (mIsRoot) { \
114 MOZ_LOG(sRootScrollbarsLog, LogLevel::Debug, (__VA_ARGS__)); \
116 static mozilla::LazyLogModule sDisplayportLog("apz.displayport");
118 using namespace mozilla;
119 using namespace mozilla::dom;
120 using namespace mozilla::gfx;
121 using namespace mozilla::layers;
122 using namespace mozilla::layout;
123 using nsStyleTransformMatrix::TransformReferenceBox;
125 static ScrollDirections GetOverflowChange(const nsRect& aCurScrolledRect,
126 const nsRect& aPrevScrolledRect) {
127 ScrollDirections result;
128 if (aPrevScrolledRect.x != aCurScrolledRect.x ||
129 aPrevScrolledRect.width != aCurScrolledRect.width) {
130 result += ScrollDirection::eHorizontal;
132 if (aPrevScrolledRect.y != aCurScrolledRect.y ||
133 aPrevScrolledRect.height != aCurScrolledRect.height) {
134 result += ScrollDirection::eVertical;
136 return result;
140 * This class handles the dispatching of scroll events to content.
142 * Scroll events are posted to the refresh driver via
143 * nsRefreshDriver::PostScrollEvent(), and they are fired during a refresh
144 * driver tick, after running requestAnimationFrame callbacks but before
145 * the style flush. This allows rAF callbacks to perform scrolling and have
146 * that scrolling be reflected on the same refresh driver tick, while at
147 * the same time allowing scroll event listeners to make style changes and
148 * have those style changes be reflected on the same refresh driver tick.
150 * ScrollEvents cannot be refresh observers, because none of the existing
151 * categories of refresh observers (FlushType::Style, FlushType::Layout,
152 * and FlushType::Display) are run at the desired time in a refresh driver
153 * tick. They behave similarly to refresh observers in that their presence
154 * causes the refresh driver to tick.
156 * ScrollEvents are one-shot runnables; the refresh driver drops them after
157 * running them.
159 class nsHTMLScrollFrame::ScrollEvent : public Runnable {
160 public:
161 NS_DECL_NSIRUNNABLE
162 explicit ScrollEvent(nsHTMLScrollFrame* aHelper, bool aDelayed);
163 void Revoke() { mHelper = nullptr; }
165 private:
166 nsHTMLScrollFrame* mHelper;
169 class nsHTMLScrollFrame::ScrollEndEvent : public Runnable {
170 public:
171 NS_DECL_NSIRUNNABLE
172 explicit ScrollEndEvent(nsHTMLScrollFrame* aHelper);
173 void Revoke() { mHelper = nullptr; }
175 private:
176 nsHTMLScrollFrame* mHelper;
179 class nsHTMLScrollFrame::AsyncScrollPortEvent : public Runnable {
180 public:
181 NS_DECL_NSIRUNNABLE
182 explicit AsyncScrollPortEvent(nsHTMLScrollFrame* helper)
183 : Runnable("nsHTMLScrollFrame::AsyncScrollPortEvent"), mHelper(helper) {}
184 void Revoke() { mHelper = nullptr; }
186 private:
187 nsHTMLScrollFrame* mHelper;
190 class nsHTMLScrollFrame::ScrolledAreaEvent : public Runnable {
191 public:
192 NS_DECL_NSIRUNNABLE
193 explicit ScrolledAreaEvent(nsHTMLScrollFrame* helper)
194 : Runnable("nsHTMLScrollFrame::ScrolledAreaEvent"), mHelper(helper) {}
195 void Revoke() { mHelper = nullptr; }
197 private:
198 nsHTMLScrollFrame* mHelper;
201 //----------------------------------------------------------------------
203 //----------nsHTMLScrollFrame-------------------------------------------
205 class ScrollFrameActivityTracker final
206 : public nsExpirationTracker<nsHTMLScrollFrame, 4> {
207 public:
208 // Wait for 3-4s between scrolls before we remove our layers.
209 // That's 4 generations of 1s each.
210 enum { TIMEOUT_MS = 1000 };
211 explicit ScrollFrameActivityTracker(nsIEventTarget* aEventTarget)
212 : nsExpirationTracker<nsHTMLScrollFrame, 4>(
213 TIMEOUT_MS, "ScrollFrameActivityTracker", aEventTarget) {}
214 ~ScrollFrameActivityTracker() { AgeAllGenerations(); }
216 virtual void NotifyExpired(nsHTMLScrollFrame* aObject) override {
217 RemoveObject(aObject);
218 aObject->MarkNotRecentlyScrolled();
221 static StaticAutoPtr<ScrollFrameActivityTracker> gScrollFrameActivityTracker;
223 nsHTMLScrollFrame* NS_NewHTMLScrollFrame(PresShell* aPresShell,
224 ComputedStyle* aStyle, bool aIsRoot) {
225 return new (aPresShell)
226 nsHTMLScrollFrame(aStyle, aPresShell->GetPresContext(), aIsRoot);
229 NS_IMPL_FRAMEARENA_HELPERS(nsHTMLScrollFrame)
231 nsHTMLScrollFrame::nsHTMLScrollFrame(ComputedStyle* aStyle,
232 nsPresContext* aPresContext,
233 nsIFrame::ClassID aID, bool aIsRoot)
234 : nsContainerFrame(aStyle, aPresContext, aID),
235 mHScrollbarBox(nullptr),
236 mVScrollbarBox(nullptr),
237 mScrolledFrame(nullptr),
238 mScrollCornerBox(nullptr),
239 mResizerBox(nullptr),
240 mReferenceFrameDuringPainting(nullptr),
241 mAsyncScroll(nullptr),
242 mAsyncSmoothMSDScroll(nullptr),
243 mLastScrollOrigin(ScrollOrigin::None),
244 mDestination(0, 0),
245 mRestorePos(-1, -1),
246 mLastPos(-1, -1),
247 mApzScrollPos(0, 0),
248 mLastUpdateFramesPos(-1, -1),
249 mScrollParentID(mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID),
250 mAnchor(this),
251 mCurrentAPZScrollAnimationType(APZScrollAnimationType::No),
252 mIsFirstScrollableFrameSequenceNumber(Nothing()),
253 mInScrollingGesture(InScrollingGesture::No),
254 mAllowScrollOriginDowngrade(false),
255 mHadDisplayPortAtLastFrameUpdate(false),
256 mHasVerticalScrollbar(false),
257 mHasHorizontalScrollbar(false),
258 mOnlyNeedVScrollbarToScrollVVInsideLV(false),
259 mOnlyNeedHScrollbarToScrollVVInsideLV(false),
260 mFrameIsUpdatingScrollbar(false),
261 mDidHistoryRestore(false),
262 mIsRoot(aIsRoot),
263 mSuppressScrollbarUpdate(false),
264 mSkippedScrollbarLayout(false),
265 mHadNonInitialReflow(false),
266 mFirstReflow(true),
267 mHorizontalOverflow(false),
268 mVerticalOverflow(false),
269 mPostedReflowCallback(false),
270 mMayHaveDirtyFixedChildren(false),
271 mUpdateScrollbarAttributes(false),
272 mHasBeenScrolledRecently(false),
273 mWillBuildScrollableLayer(false),
274 mIsParentToActiveScrollFrames(false),
275 mHasBeenScrolled(false),
276 mIgnoreMomentumScroll(false),
277 mTransformingByAPZ(false),
278 mScrollableByAPZ(false),
279 mZoomableByAPZ(false),
280 mHasOutOfFlowContentInsideFilter(false),
281 mSuppressScrollbarRepaints(false),
282 mIsUsingMinimumScaleSize(false),
283 mMinimumScaleSizeChanged(false),
284 mProcessingScrollEvent(false),
285 mApzAnimationRequested(false),
286 mApzAnimationTriggeredByScriptRequested(false),
287 mReclampVVOffsetInReflowFinished(false),
288 mMayScheduleScrollAnimations(false),
289 #ifdef MOZ_WIDGET_ANDROID
290 mHasVerticalOverflowForDynamicToolbar(false),
291 #endif
292 mVelocityQueue(PresContext()) {
293 AppendScrollUpdate(ScrollPositionUpdate::NewScrollframe(nsPoint()));
295 if (UsesOverlayScrollbars()) {
296 mScrollbarActivity = new ScrollbarActivity(this);
299 if (mIsRoot) {
300 mZoomableByAPZ = PresShell()->GetZoomableByAPZ();
304 nsHTMLScrollFrame::~nsHTMLScrollFrame() = default;
306 void nsHTMLScrollFrame::ScrollbarActivityStarted() const {
307 if (mScrollbarActivity) {
308 mScrollbarActivity->ActivityStarted();
312 void nsHTMLScrollFrame::ScrollbarActivityStopped() const {
313 if (mScrollbarActivity) {
314 mScrollbarActivity->ActivityStopped();
318 void nsHTMLScrollFrame::Destroy(DestroyContext& aContext) {
319 DestroyAbsoluteFrames(aContext);
320 if (mIsRoot) {
321 PresShell()->ResetVisualViewportOffset();
324 mAnchor.Destroy();
326 if (mScrollbarActivity) {
327 mScrollbarActivity->Destroy();
328 mScrollbarActivity = nullptr;
331 // Unbind the content created in CreateAnonymousContent later...
332 aContext.AddAnonymousContent(mHScrollbarContent.forget());
333 aContext.AddAnonymousContent(mVScrollbarContent.forget());
334 aContext.AddAnonymousContent(mScrollCornerContent.forget());
335 aContext.AddAnonymousContent(mResizerContent.forget());
337 if (mPostedReflowCallback) {
338 PresShell()->CancelReflowCallback(this);
339 mPostedReflowCallback = false;
342 if (mDisplayPortExpiryTimer) {
343 mDisplayPortExpiryTimer->Cancel();
344 mDisplayPortExpiryTimer = nullptr;
346 if (mActivityExpirationState.IsTracked()) {
347 gScrollFrameActivityTracker->RemoveObject(this);
349 if (gScrollFrameActivityTracker && gScrollFrameActivityTracker->IsEmpty()) {
350 gScrollFrameActivityTracker = nullptr;
353 if (mScrollActivityTimer) {
354 mScrollActivityTimer->Cancel();
355 mScrollActivityTimer = nullptr;
357 RemoveObservers();
358 if (mScrollEvent) {
359 mScrollEvent->Revoke();
361 if (mScrollEndEvent) {
362 mScrollEndEvent->Revoke();
364 nsContainerFrame::Destroy(aContext);
367 void nsHTMLScrollFrame::SetInitialChildList(ChildListID aListID,
368 nsFrameList&& aChildList) {
369 nsContainerFrame::SetInitialChildList(aListID, std::move(aChildList));
370 ReloadChildFrames();
373 void nsHTMLScrollFrame::AppendFrames(ChildListID aListID,
374 nsFrameList&& aFrameList) {
375 NS_ASSERTION(aListID == FrameChildListID::Principal,
376 "Only main list supported");
377 mFrames.AppendFrames(nullptr, std::move(aFrameList));
378 ReloadChildFrames();
381 void nsHTMLScrollFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame,
382 const nsLineList::iterator* aPrevFrameLine,
383 nsFrameList&& aFrameList) {
384 NS_ASSERTION(aListID == FrameChildListID::Principal,
385 "Only main list supported");
386 NS_ASSERTION(!aPrevFrame || aPrevFrame->GetParent() == this,
387 "inserting after sibling frame with different parent");
388 mFrames.InsertFrames(nullptr, aPrevFrame, std::move(aFrameList));
389 ReloadChildFrames();
392 void nsHTMLScrollFrame::RemoveFrame(DestroyContext& aContext,
393 ChildListID aListID, nsIFrame* aOldFrame) {
394 NS_ASSERTION(aListID == FrameChildListID::Principal,
395 "Only main list supported");
396 mFrames.DestroyFrame(aContext, aOldFrame);
397 ReloadChildFrames();
401 HTML scrolling implementation
403 All other things being equal, we prefer layouts with fewer scrollbars showing.
406 namespace mozilla {
408 enum class ShowScrollbar : uint8_t {
409 Auto,
410 Always,
411 // Never is a misnomer. We can still get a scrollbar if we need to scroll the
412 // visual viewport inside the layout viewport. Thus this enum is best thought
413 // of as value used by layout, which does not know about the visual viewport.
414 // The visual viewport does not affect any layout sizes, so this is sound.
415 Never,
418 static ShowScrollbar ShouldShowScrollbar(StyleOverflow aOverflow) {
419 switch (aOverflow) {
420 case StyleOverflow::Scroll:
421 return ShowScrollbar::Always;
422 case StyleOverflow::Hidden:
423 return ShowScrollbar::Never;
424 default:
425 case StyleOverflow::Auto:
426 return ShowScrollbar::Auto;
430 struct MOZ_STACK_CLASS ScrollReflowInput {
431 // === Filled in by the constructor. Members in this section shouldn't change
432 // their values after the constructor. ===
433 const ReflowInput& mReflowInput;
434 ShowScrollbar mHScrollbar;
435 // If the horizontal scrollbar is allowed (even if mHScrollbar ==
436 // ShowScrollbar::Never) provided that it is for scrolling the visual viewport
437 // inside the layout viewport only.
438 bool mHScrollbarAllowedForScrollingVVInsideLV = true;
439 ShowScrollbar mVScrollbar;
440 // If the vertical scrollbar is allowed (even if mVScrollbar ==
441 // ShowScrollbar::Never) provided that it is for scrolling the visual viewport
442 // inside the layout viewport only.
443 bool mVScrollbarAllowedForScrollingVVInsideLV = true;
444 nsMargin mComputedBorder;
446 // === Filled in by ReflowScrolledFrame ===
447 OverflowAreas mContentsOverflowAreas;
448 // The scrollbar gutter sizes used in the most recent reflow of
449 // mScrolledFrame. The writing-mode is the same as the scroll
450 // container.
451 LogicalMargin mScrollbarGutterFromLastReflow;
452 // True if the most recent reflow of mScrolledFrame is with the
453 // horizontal scrollbar.
454 bool mReflowedContentsWithHScrollbar = false;
455 // True if the most recent reflow of mScrolledFrame is with the
456 // vertical scrollbar.
457 bool mReflowedContentsWithVScrollbar = false;
459 // === Filled in when TryLayout succeeds ===
460 // The size of the inside-border area
461 nsSize mInsideBorderSize;
462 // Whether we decided to show the horizontal scrollbar in the most recent
463 // TryLayout.
464 bool mShowHScrollbar = false;
465 // Whether we decided to show the vertical scrollbar in the most recent
466 // TryLayout.
467 bool mShowVScrollbar = false;
468 // If mShow(H|V)Scrollbar is true then
469 // mOnlyNeed(V|H)ScrollbarToScrollVVInsideLV indicates if the only reason we
470 // need that scrollbar is to scroll the visual viewport inside the layout
471 // viewport. These scrollbars are special in that even if they are layout
472 // scrollbars they do not take up any layout space.
473 bool mOnlyNeedHScrollbarToScrollVVInsideLV = false;
474 bool mOnlyNeedVScrollbarToScrollVVInsideLV = false;
476 ScrollReflowInput(nsHTMLScrollFrame* aFrame, const ReflowInput& aReflowInput);
478 nscoord VScrollbarMinHeight() const { return mVScrollbarPrefSize.height; }
479 nscoord VScrollbarPrefWidth() const { return mVScrollbarPrefSize.width; }
480 nscoord HScrollbarMinWidth() const { return mHScrollbarPrefSize.width; }
481 nscoord HScrollbarPrefHeight() const { return mHScrollbarPrefSize.height; }
483 // Returns the sizes occupied by the scrollbar gutters. If aShowVScroll or
484 // aShowHScroll is true, the sizes occupied by the scrollbars are also
485 // included.
486 nsMargin ScrollbarGutter(bool aShowVScrollbar, bool aShowHScrollbar,
487 bool aScrollbarOnRight) const {
488 if (mOverlayScrollbars) {
489 return mScrollbarGutter;
491 nsMargin gutter = mScrollbarGutter;
492 if (aShowVScrollbar && gutter.right == 0 && gutter.left == 0) {
493 const nscoord w = VScrollbarPrefWidth();
494 if (aScrollbarOnRight) {
495 gutter.right = w;
496 } else {
497 gutter.left = w;
500 if (aShowHScrollbar && gutter.bottom == 0) {
501 // The horizontal scrollbar is always at the bottom side.
502 gutter.bottom = HScrollbarPrefHeight();
504 return gutter;
507 bool OverlayScrollbars() const { return mOverlayScrollbars; }
509 private:
510 // Filled in by the constructor. Put variables here to keep them unchanged
511 // after initializing them in the constructor.
512 nsSize mVScrollbarPrefSize;
513 nsSize mHScrollbarPrefSize;
514 bool mOverlayScrollbars = false;
515 // The scrollbar gutter sizes resolved from the scrollbar-gutter and
516 // scrollbar-width property.
517 nsMargin mScrollbarGutter;
520 ScrollReflowInput::ScrollReflowInput(nsHTMLScrollFrame* aFrame,
521 const ReflowInput& aReflowInput)
522 : mReflowInput(aReflowInput),
523 mComputedBorder(aReflowInput.ComputedPhysicalBorderPadding() -
524 aReflowInput.ComputedPhysicalPadding()),
525 mScrollbarGutterFromLastReflow(aFrame->GetWritingMode()) {
526 ScrollStyles styles = aFrame->GetScrollStyles();
527 mHScrollbar = ShouldShowScrollbar(styles.mHorizontal);
528 mVScrollbar = ShouldShowScrollbar(styles.mVertical);
529 mOverlayScrollbars = aFrame->UsesOverlayScrollbars();
531 if (nsScrollbarFrame* scrollbar = aFrame->GetScrollbarBox(false)) {
532 scrollbar->SetScrollbarMediatorContent(mReflowInput.mFrame->GetContent());
533 mHScrollbarPrefSize = scrollbar->ScrollbarMinSize();
534 // A zero minimum size is a bug with non-overlay scrollbars. That means
535 // we'll always try to place the scrollbar, even if it will ultimately not
536 // fit, see bug 1809630. XUL collapsing is the exception because the
537 // front-end uses it.
538 MOZ_ASSERT(mHScrollbarPrefSize.width && mHScrollbarPrefSize.height,
539 "Shouldn't have a zero horizontal scrollbar-size");
540 } else {
541 mHScrollbar = ShowScrollbar::Never;
542 mHScrollbarAllowedForScrollingVVInsideLV = false;
544 if (nsScrollbarFrame* scrollbar = aFrame->GetScrollbarBox(true)) {
545 scrollbar->SetScrollbarMediatorContent(mReflowInput.mFrame->GetContent());
546 mVScrollbarPrefSize = scrollbar->ScrollbarMinSize();
547 // See above.
548 MOZ_ASSERT(mVScrollbarPrefSize.width && mVScrollbarPrefSize.height,
549 "Shouldn't have a zero vertical scrollbar-size");
550 } else {
551 mVScrollbar = ShowScrollbar::Never;
552 mVScrollbarAllowedForScrollingVVInsideLV = false;
555 const auto* scrollbarStyle =
556 nsLayoutUtils::StyleForScrollbar(mReflowInput.mFrame);
557 // Hide the scrollbar when the scrollbar-width is set to none.
559 // Note: In some cases this is unnecessary, because scrollbar-width:none
560 // makes us suppress scrollbars in CreateAnonymousContent. But if this frame
561 // initially had a non-'none' scrollbar-width and dynamically changed to
562 // 'none', then we'll need to handle it here.
563 const auto scrollbarWidth = scrollbarStyle->StyleUIReset()->ScrollbarWidth();
564 if (scrollbarWidth == StyleScrollbarWidth::None) {
565 mHScrollbar = ShowScrollbar::Never;
566 mHScrollbarAllowedForScrollingVVInsideLV = false;
567 mVScrollbar = ShowScrollbar::Never;
568 mVScrollbarAllowedForScrollingVVInsideLV = false;
569 } else if (const auto& scrollbarGutterStyle =
570 scrollbarStyle->StyleDisplay()->mScrollbarGutter;
571 scrollbarGutterStyle && !mOverlayScrollbars) {
572 const auto stable =
573 bool(scrollbarGutterStyle & StyleScrollbarGutter::STABLE);
574 const auto bothEdges =
575 bool(scrollbarGutterStyle & StyleScrollbarGutter::BOTH_EDGES);
577 const nscoord scrollbarSize = nsHTMLScrollFrame::GetNonOverlayScrollbarSize(
578 aFrame->PresContext(), scrollbarWidth);
579 if (mReflowInput.GetWritingMode().IsVertical()) {
580 if (bothEdges) {
581 mScrollbarGutter.top = mScrollbarGutter.bottom = scrollbarSize;
582 } else if (stable) {
583 // The horizontal scrollbar gutter is always at the bottom side.
584 mScrollbarGutter.bottom = scrollbarSize;
586 } else {
587 if (bothEdges) {
588 mScrollbarGutter.left = mScrollbarGutter.right = scrollbarSize;
589 } else if (stable) {
590 if (aFrame->IsScrollbarOnRight()) {
591 mScrollbarGutter.right = scrollbarSize;
592 } else {
593 mScrollbarGutter.left = scrollbarSize;
600 } // namespace mozilla
602 // XXXldb Can this go away?
603 static nsSize ComputeInsideBorderSize(const ScrollReflowInput& aState,
604 const nsSize& aDesiredInsideBorderSize) {
605 // aDesiredInsideBorderSize is the frame size; i.e., it includes
606 // borders and padding (but the scrolled child doesn't have
607 // borders). The scrolled child has the same padding as us.
608 nscoord contentWidth = aState.mReflowInput.ComputedWidth();
609 if (contentWidth == NS_UNCONSTRAINEDSIZE) {
610 contentWidth = aDesiredInsideBorderSize.width -
611 aState.mReflowInput.ComputedPhysicalPadding().LeftRight();
613 nscoord contentHeight = aState.mReflowInput.ComputedHeight();
614 if (contentHeight == NS_UNCONSTRAINEDSIZE) {
615 contentHeight = aDesiredInsideBorderSize.height -
616 aState.mReflowInput.ComputedPhysicalPadding().TopBottom();
619 contentWidth = aState.mReflowInput.ApplyMinMaxWidth(contentWidth);
620 contentHeight = aState.mReflowInput.ApplyMinMaxHeight(contentHeight);
621 return nsSize(
622 contentWidth + aState.mReflowInput.ComputedPhysicalPadding().LeftRight(),
623 contentHeight +
624 aState.mReflowInput.ComputedPhysicalPadding().TopBottom());
628 * Assuming that we know the metrics for our wrapped frame and
629 * whether the horizontal and/or vertical scrollbars are present,
630 * compute the resulting layout and return true if the layout is
631 * consistent. If the layout is consistent then we fill in the
632 * computed fields of the ScrollReflowInput.
634 * The layout is consistent when both scrollbars are showing if and only
635 * if they should be showing. A horizontal scrollbar should be showing if all
636 * following conditions are met:
637 * 1) the style is not HIDDEN
638 * 2) our inside-border height is at least the scrollbar height (i.e., the
639 * scrollbar fits vertically)
640 * 3) the style is SCROLL, or the kid's overflow-area XMost is
641 * greater than the scrollport width
643 * @param aForce if true, then we just assume the layout is consistent.
645 bool nsHTMLScrollFrame::TryLayout(ScrollReflowInput& aState,
646 ReflowOutput* aKidMetrics,
647 bool aAssumeHScroll, bool aAssumeVScroll,
648 bool aForce) {
649 if ((aState.mVScrollbar == ShowScrollbar::Never && aAssumeVScroll) ||
650 (aState.mHScrollbar == ShowScrollbar::Never && aAssumeHScroll)) {
651 NS_ASSERTION(!aForce, "Shouldn't be forcing a hidden scrollbar to show!");
652 return false;
655 const auto wm = GetWritingMode();
656 const nsMargin scrollbarGutter = aState.ScrollbarGutter(
657 aAssumeVScroll, aAssumeHScroll, IsScrollbarOnRight());
658 const LogicalMargin logicalScrollbarGutter(wm, scrollbarGutter);
660 const bool inlineEndsGutterChanged =
661 aState.mScrollbarGutterFromLastReflow.IStartEnd(wm) !=
662 logicalScrollbarGutter.IStartEnd(wm);
663 const bool blockEndsGutterChanged =
664 aState.mScrollbarGutterFromLastReflow.BStartEnd(wm) !=
665 logicalScrollbarGutter.BStartEnd(wm);
666 const bool shouldReflowScrolledFrame =
667 inlineEndsGutterChanged ||
668 (blockEndsGutterChanged && ScrolledContentDependsOnBSize(aState));
670 if (shouldReflowScrolledFrame) {
671 if (blockEndsGutterChanged) {
672 nsLayoutUtils::MarkIntrinsicISizesDirtyIfDependentOnBSize(mScrolledFrame);
674 aKidMetrics->mOverflowAreas.Clear();
675 ROOT_SCROLLBAR_LOG(
676 "TryLayout reflowing scrolled frame with scrollbars h=%d, v=%d\n",
677 aAssumeHScroll, aAssumeVScroll);
678 ReflowScrolledFrame(aState, aAssumeHScroll, aAssumeVScroll, aKidMetrics);
681 const nsSize scrollbarGutterSize(scrollbarGutter.LeftRight(),
682 scrollbarGutter.TopBottom());
684 // First, compute our inside-border size and scrollport size
685 // XXXldb Can we depend more on ComputeSize here?
686 nsSize kidSize = GetContainSizeAxes().ContainSize(
687 aKidMetrics->PhysicalSize(), *aState.mReflowInput.mFrame);
688 const nsSize desiredInsideBorderSize = kidSize + scrollbarGutterSize;
689 aState.mInsideBorderSize =
690 ComputeInsideBorderSize(aState, desiredInsideBorderSize);
692 nsSize layoutSize =
693 mIsUsingMinimumScaleSize ? mMinimumScaleSize : aState.mInsideBorderSize;
695 const nsSize scrollPortSize =
696 Max(nsSize(0, 0), layoutSize - scrollbarGutterSize);
697 if (mIsUsingMinimumScaleSize) {
698 mICBSize =
699 Max(nsSize(0, 0), aState.mInsideBorderSize - scrollbarGutterSize);
702 nsSize visualViewportSize = scrollPortSize;
703 ROOT_SCROLLBAR_LOG("TryLayout with VV %s\n",
704 ToString(visualViewportSize).c_str());
705 mozilla::PresShell* presShell = PresShell();
706 // Note: we check for a non-null MobileViepwortManager here, but ideally we
707 // should be able to drop that clause as well. It's just that in some cases
708 // with extension popups the composition size comes back as stale, because
709 // the content viewer is only resized after the popup contents are reflowed.
710 // That case also happens to have no APZ and no MVM, so we use that as a
711 // way to detect the scenario. Bug 1648669 tracks removing this clause.
712 if (mIsRoot && presShell->GetMobileViewportManager()) {
713 visualViewportSize = nsLayoutUtils::CalculateCompositionSizeForFrame(
714 this, false, &layoutSize);
715 visualViewportSize =
716 Max(nsSize(0, 0), visualViewportSize - scrollbarGutterSize);
718 float resolution = presShell->GetResolution();
719 visualViewportSize.width /= resolution;
720 visualViewportSize.height /= resolution;
721 ROOT_SCROLLBAR_LOG("TryLayout now with VV %s\n",
722 ToString(visualViewportSize).c_str());
725 nsRect overflowRect = aState.mContentsOverflowAreas.ScrollableOverflow();
726 // If the content height expanded by the minimum-scale will be taller than
727 // the scrollable overflow area, we need to expand the area here to tell
728 // properly whether we need to render the overlay vertical scrollbar.
729 // NOTE: This expanded size should NOT be used for non-overley scrollbars
730 // cases since putting the vertical non-overlay scrollbar will make the
731 // content width narrow a little bit, which in turn the minimum scale value
732 // becomes a bit bigger than before, then the vertical scrollbar is no longer
733 // needed, which means the content width becomes the original width, then the
734 // minimum-scale is changed to the original one, and so forth.
735 if (UsesOverlayScrollbars() && mIsUsingMinimumScaleSize &&
736 mMinimumScaleSize.height > overflowRect.YMost()) {
737 overflowRect.height += mMinimumScaleSize.height - overflowRect.YMost();
739 nsRect scrolledRect =
740 GetUnsnappedScrolledRectInternal(overflowRect, scrollPortSize);
741 ROOT_SCROLLBAR_LOG(
742 "TryLayout scrolledRect:%s overflowRect:%s scrollportSize:%s\n",
743 ToString(scrolledRect).c_str(), ToString(overflowRect).c_str(),
744 ToString(scrollPortSize).c_str());
745 nscoord oneDevPixel = PresContext()->DevPixelsToAppUnits(1);
747 bool showHScrollbar = aAssumeHScroll;
748 bool showVScrollbar = aAssumeVScroll;
749 if (!aForce) {
750 nsSize sizeToCompare = visualViewportSize;
751 if (gfxPlatform::UseDesktopZoomingScrollbars()) {
752 sizeToCompare = scrollPortSize;
755 // No need to compute showHScrollbar if we got ShowScrollbar::Never.
756 if (aState.mHScrollbar != ShowScrollbar::Never) {
757 showHScrollbar =
758 aState.mHScrollbar == ShowScrollbar::Always ||
759 scrolledRect.XMost() >= sizeToCompare.width + oneDevPixel ||
760 scrolledRect.x <= -oneDevPixel;
761 // TODO(emilio): This should probably check this scrollbar's minimum size
762 // in both axes, for consistency?
763 if (aState.mHScrollbar == ShowScrollbar::Auto &&
764 scrollPortSize.width < aState.HScrollbarMinWidth()) {
765 showHScrollbar = false;
767 ROOT_SCROLLBAR_LOG("TryLayout wants H Scrollbar: %d =? %d\n",
768 showHScrollbar, aAssumeHScroll);
771 // No need to compute showVScrollbar if we got ShowScrollbar::Never.
772 if (aState.mVScrollbar != ShowScrollbar::Never) {
773 showVScrollbar =
774 aState.mVScrollbar == ShowScrollbar::Always ||
775 scrolledRect.YMost() >= sizeToCompare.height + oneDevPixel ||
776 scrolledRect.y <= -oneDevPixel;
777 // TODO(emilio): This should probably check this scrollbar's minimum size
778 // in both axes, for consistency?
779 if (aState.mVScrollbar == ShowScrollbar::Auto &&
780 scrollPortSize.height < aState.VScrollbarMinHeight()) {
781 showVScrollbar = false;
783 ROOT_SCROLLBAR_LOG("TryLayout wants V Scrollbar: %d =? %d\n",
784 showVScrollbar, aAssumeVScroll);
787 if (showHScrollbar != aAssumeHScroll || showVScrollbar != aAssumeVScroll) {
788 const nsMargin wantedScrollbarGutter = aState.ScrollbarGutter(
789 showVScrollbar, showHScrollbar, IsScrollbarOnRight());
790 // We report an inconsistent layout only when the desired visibility of
791 // the scrollbars can change the size of the scrollbar gutters.
792 if (scrollbarGutter != wantedScrollbarGutter) {
793 return false;
798 // If we reach here, the layout is consistent. Record the desired visibility
799 // of the scrollbars.
800 aState.mShowHScrollbar = showHScrollbar;
801 aState.mShowVScrollbar = showVScrollbar;
802 const nsPoint scrollPortOrigin(
803 aState.mComputedBorder.left + scrollbarGutter.left,
804 aState.mComputedBorder.top + scrollbarGutter.top);
805 SetScrollPort(nsRect(scrollPortOrigin, scrollPortSize));
807 if (mIsRoot && gfxPlatform::UseDesktopZoomingScrollbars()) {
808 bool vvChanged = true;
809 const bool overlay = aState.OverlayScrollbars();
810 // This loop can run at most twice since we can only add a scrollbar once.
811 // At this point we've already decided that this layout is consistent so we
812 // will return true. Scrollbars added here never take up layout space even
813 // if they are layout scrollbars so any changes made here will not make us
814 // return false.
815 while (vvChanged) {
816 vvChanged = false;
817 if (!aState.mShowHScrollbar &&
818 aState.mHScrollbarAllowedForScrollingVVInsideLV) {
819 if (ScrollPort().width >= visualViewportSize.width + oneDevPixel &&
820 (overlay ||
821 visualViewportSize.width >= aState.HScrollbarMinWidth())) {
822 vvChanged = true;
823 if (!overlay) {
824 visualViewportSize.height -= aState.HScrollbarPrefHeight();
826 aState.mShowHScrollbar = true;
827 aState.mOnlyNeedHScrollbarToScrollVVInsideLV = true;
828 ROOT_SCROLLBAR_LOG("TryLayout added H scrollbar for VV, VV now %s\n",
829 ToString(visualViewportSize).c_str());
833 if (!aState.mShowVScrollbar &&
834 aState.mVScrollbarAllowedForScrollingVVInsideLV) {
835 if (ScrollPort().height >= visualViewportSize.height + oneDevPixel &&
836 (overlay ||
837 visualViewportSize.height >= aState.VScrollbarMinHeight())) {
838 vvChanged = true;
839 if (!overlay) {
840 visualViewportSize.width -= aState.VScrollbarPrefWidth();
842 aState.mShowVScrollbar = true;
843 aState.mOnlyNeedVScrollbarToScrollVVInsideLV = true;
844 ROOT_SCROLLBAR_LOG("TryLayout added V scrollbar for VV, VV now %s\n",
845 ToString(visualViewportSize).c_str());
851 return true;
854 bool nsHTMLScrollFrame::ScrolledContentDependsOnBSize(
855 const ScrollReflowInput& aState) const {
856 return mScrolledFrame->HasAnyStateBits(
857 NS_FRAME_CONTAINS_RELATIVE_BSIZE |
858 NS_FRAME_DESCENDANT_INTRINSIC_ISIZE_DEPENDS_ON_BSIZE) ||
859 aState.mReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE ||
860 aState.mReflowInput.ComputedMinBSize() > 0 ||
861 aState.mReflowInput.ComputedMaxBSize() != NS_UNCONSTRAINEDSIZE;
864 void nsHTMLScrollFrame::ReflowScrolledFrame(ScrollReflowInput& aState,
865 bool aAssumeHScroll,
866 bool aAssumeVScroll,
867 ReflowOutput* aMetrics) {
868 const WritingMode wm = GetWritingMode();
870 // these could be NS_UNCONSTRAINEDSIZE ... std::min arithmetic should
871 // be OK
872 LogicalMargin padding = aState.mReflowInput.ComputedLogicalPadding(wm);
873 nscoord availISize =
874 aState.mReflowInput.ComputedISize() + padding.IStartEnd(wm);
876 nscoord computedBSize = aState.mReflowInput.ComputedBSize();
877 nscoord computedMinBSize = aState.mReflowInput.ComputedMinBSize();
878 nscoord computedMaxBSize = aState.mReflowInput.ComputedMaxBSize();
879 if (!ShouldPropagateComputedBSizeToScrolledContent()) {
880 computedBSize = NS_UNCONSTRAINEDSIZE;
881 computedMinBSize = 0;
882 computedMaxBSize = NS_UNCONSTRAINEDSIZE;
885 const LogicalMargin scrollbarGutter(
886 wm, aState.ScrollbarGutter(aAssumeVScroll, aAssumeHScroll,
887 IsScrollbarOnRight()));
888 if (const nscoord inlineEndsGutter = scrollbarGutter.IStartEnd(wm);
889 inlineEndsGutter > 0) {
890 availISize = std::max(0, availISize - inlineEndsGutter);
892 if (const nscoord blockEndsGutter = scrollbarGutter.BStartEnd(wm);
893 blockEndsGutter > 0) {
894 if (computedBSize != NS_UNCONSTRAINEDSIZE) {
895 computedBSize = std::max(0, computedBSize - blockEndsGutter);
897 computedMinBSize = std::max(0, computedMinBSize - blockEndsGutter);
898 if (computedMaxBSize != NS_UNCONSTRAINEDSIZE) {
899 computedMaxBSize = std::max(0, computedMaxBSize - blockEndsGutter);
903 nsPresContext* presContext = PresContext();
905 // Pass InitFlags::CallerWillInit so we can pass in the correct padding.
906 ReflowInput kidReflowInput(presContext, aState.mReflowInput, mScrolledFrame,
907 LogicalSize(wm, availISize, NS_UNCONSTRAINEDSIZE),
908 Nothing(), ReflowInput::InitFlag::CallerWillInit);
909 const WritingMode kidWM = kidReflowInput.GetWritingMode();
910 kidReflowInput.Init(presContext, Nothing(), Nothing(),
911 Some(padding.ConvertTo(kidWM, wm)));
912 kidReflowInput.mFlags.mAssumingHScrollbar = aAssumeHScroll;
913 kidReflowInput.mFlags.mAssumingVScrollbar = aAssumeVScroll;
914 kidReflowInput.mFlags.mTreatBSizeAsIndefinite =
915 aState.mReflowInput.mFlags.mTreatBSizeAsIndefinite;
916 kidReflowInput.SetComputedBSize(computedBSize);
917 kidReflowInput.SetComputedMinBSize(computedMinBSize);
918 kidReflowInput.SetComputedMaxBSize(computedMaxBSize);
919 if (aState.mReflowInput.IsBResizeForWM(kidWM)) {
920 kidReflowInput.SetBResize(true);
922 if (aState.mReflowInput.IsBResizeForPercentagesForWM(kidWM)) {
923 kidReflowInput.mFlags.mIsBResizeForPercentages = true;
926 // Temporarily set mHasHorizontalScrollbar/mHasVerticalScrollbar to
927 // reflect our assumptions while we reflow the child.
928 bool didHaveHorizontalScrollbar = mHasHorizontalScrollbar;
929 bool didHaveVerticalScrollbar = mHasVerticalScrollbar;
930 mHasHorizontalScrollbar = aAssumeHScroll;
931 mHasVerticalScrollbar = aAssumeVScroll;
933 nsReflowStatus status;
934 // No need to pass a true container-size to ReflowChild or
935 // FinishReflowChild, because it's only used there when positioning
936 // the frame (i.e. if ReflowChildFlags::NoMoveFrame isn't set)
937 const nsSize dummyContainerSize;
938 ReflowChild(mScrolledFrame, presContext, *aMetrics, kidReflowInput, wm,
939 LogicalPoint(wm), dummyContainerSize,
940 ReflowChildFlags::NoMoveFrame, status);
942 mHasHorizontalScrollbar = didHaveHorizontalScrollbar;
943 mHasVerticalScrollbar = didHaveVerticalScrollbar;
945 // Don't resize or position the view (if any) because we're going to resize
946 // it to the correct size anyway in PlaceScrollArea. Allowing it to
947 // resize here would size it to the natural height of the frame,
948 // which will usually be different from the scrollport height;
949 // invalidating the difference will cause unnecessary repainting.
950 FinishReflowChild(
951 mScrolledFrame, presContext, *aMetrics, &kidReflowInput, wm,
952 LogicalPoint(wm), dummyContainerSize,
953 ReflowChildFlags::NoMoveFrame | ReflowChildFlags::NoSizeView);
955 if (mScrolledFrame->HasAnyStateBits(NS_FRAME_CONTAINS_RELATIVE_BSIZE)) {
956 // Propagate NS_FRAME_CONTAINS_RELATIVE_BSIZE from our inner scrolled frame
957 // to ourselves so that our containing block is aware of it.
959 // Note: If the scrolled frame has any child whose block-size depends on the
960 // containing block's block-size, the NS_FRAME_CONTAINS_RELATIVE_BSIZE bit
961 // is set on the scrolled frame when initializing the child's ReflowInput in
962 // ReflowInput::InitResizeFlags(). Therefore, we propagate the bit here
963 // after we reflowed the scrolled frame.
964 AddStateBits(NS_FRAME_CONTAINS_RELATIVE_BSIZE);
967 // XXX Some frames (e.g. nsFrameFrame, nsTextFrame) don't
968 // bother setting their mOverflowArea. This is wrong because every frame
969 // should always set mOverflowArea. In fact nsFrameFrame doesn't
970 // support the 'outline' property because of this. Rather than fix the
971 // world right now, just fix up the overflow area if necessary. Note that we
972 // don't check HasOverflowRect() because it could be set even though the
973 // overflow area doesn't include the frame bounds.
974 aMetrics->UnionOverflowAreasWithDesiredBounds();
976 auto* disp = StyleDisplay();
977 if (MOZ_UNLIKELY(disp->mOverflowClipBoxInline ==
978 StyleOverflowClipBox::ContentBox)) {
979 // The scrolled frame is scrollable in the inline axis with
980 // `overflow-clip-box:content-box`. To prevent its content from being
981 // clipped at the scroll container's padding edges, we inflate its
982 // children's scrollable overflow area with its inline padding, and union
983 // its scrollable overflow area with its children's inflated scrollable
984 // overflow area.
985 OverflowAreas childOverflow;
986 mScrolledFrame->UnionChildOverflow(childOverflow);
987 nsRect childScrollableOverflow = childOverflow.ScrollableOverflow();
989 const LogicalMargin inlinePadding =
990 padding.ApplySkipSides(LogicalSides(wm, eLogicalSideBitsBBoth));
991 childScrollableOverflow.Inflate(inlinePadding.GetPhysicalMargin(wm));
993 nsRect& so = aMetrics->ScrollableOverflow();
994 so = so.UnionEdges(childScrollableOverflow);
997 aState.mContentsOverflowAreas = aMetrics->mOverflowAreas;
998 aState.mScrollbarGutterFromLastReflow = scrollbarGutter;
999 aState.mReflowedContentsWithHScrollbar = aAssumeHScroll;
1000 aState.mReflowedContentsWithVScrollbar = aAssumeVScroll;
1003 bool nsHTMLScrollFrame::GuessHScrollbarNeeded(const ScrollReflowInput& aState) {
1004 if (aState.mHScrollbar != ShowScrollbar::Auto) {
1005 // no guessing required
1006 return aState.mHScrollbar == ShowScrollbar::Always;
1008 // We only care about scrollbars that might take up space when trying to guess
1009 // if we need a scrollbar, so we ignore scrollbars only created to scroll the
1010 // visual viewport inside the layout viewport because they take up no layout
1011 // space.
1012 return mHasHorizontalScrollbar && !mOnlyNeedHScrollbarToScrollVVInsideLV;
1015 bool nsHTMLScrollFrame::GuessVScrollbarNeeded(const ScrollReflowInput& aState) {
1016 if (aState.mVScrollbar != ShowScrollbar::Auto) {
1017 // no guessing required
1018 return aState.mVScrollbar == ShowScrollbar::Always;
1021 // If we've had at least one non-initial reflow, then just assume
1022 // the state of the vertical scrollbar will be what we determined
1023 // last time.
1024 if (mHadNonInitialReflow) {
1025 // We only care about scrollbars that might take up space when trying to
1026 // guess if we need a scrollbar, so we ignore scrollbars only created to
1027 // scroll the visual viewport inside the layout viewport because they take
1028 // up no layout space.
1029 return mHasVerticalScrollbar && !mOnlyNeedVScrollbarToScrollVVInsideLV;
1032 // If this is the initial reflow, guess false because usually
1033 // we have very little content by then.
1034 if (InInitialReflow()) return false;
1036 if (mIsRoot) {
1037 nsIFrame* f = mScrolledFrame->PrincipalChildList().FirstChild();
1038 if (f && f->IsSVGOuterSVGFrame() &&
1039 static_cast<SVGOuterSVGFrame*>(f)->VerticalScrollbarNotNeeded()) {
1040 // Common SVG case - avoid a bad guess.
1041 return false;
1043 // Assume that there will be a scrollbar; it seems to me
1044 // that 'most pages' do have a scrollbar, and anyway, it's cheaper
1045 // to do an extra reflow for the pages that *don't* need a
1046 // scrollbar (because on average they will have less content).
1047 return true;
1050 // For non-viewports, just guess that we don't need a scrollbar.
1051 // XXX I wonder if statistically this is the right idea; I'm
1052 // basically guessing that there are a lot of overflow:auto DIVs
1053 // that get their intrinsic size and don't overflow
1054 return false;
1057 bool nsHTMLScrollFrame::InInitialReflow() const {
1058 // We're in an initial reflow if NS_FRAME_FIRST_REFLOW is set, unless we're a
1059 // root scrollframe. In that case we want to skip this clause altogether.
1060 // The guess here is that there are lots of overflow:auto divs out there that
1061 // end up auto-sizing so they don't overflow, and that the root basically
1062 // always needs a scrollbar if it did last time we loaded this page (good
1063 // assumption, because our initial reflow is no longer synchronous).
1064 return !mIsRoot && HasAnyStateBits(NS_FRAME_FIRST_REFLOW);
1067 void nsHTMLScrollFrame::ReflowContents(ScrollReflowInput& aState,
1068 const ReflowOutput& aDesiredSize) {
1069 const WritingMode desiredWm = aDesiredSize.GetWritingMode();
1070 ReflowOutput kidDesiredSize(desiredWm);
1071 ReflowScrolledFrame(aState, GuessHScrollbarNeeded(aState),
1072 GuessVScrollbarNeeded(aState), &kidDesiredSize);
1074 // There's an important special case ... if the child appears to fit
1075 // in the inside-border rect (but overflows the scrollport), we
1076 // should try laying it out without a vertical scrollbar. It will
1077 // usually fit because making the available-width wider will not
1078 // normally make the child taller. (The only situation I can think
1079 // of is when you have a line containing %-width inline replaced
1080 // elements whose percentages sum to more than 100%, so increasing
1081 // the available width makes the line break where it was fitting
1082 // before.) If we don't treat this case specially, then we will
1083 // decide that showing scrollbars is OK because the content
1084 // overflows when we're showing scrollbars and we won't try to
1085 // remove the vertical scrollbar.
1087 // Detecting when we enter this special case is important for when
1088 // people design layouts that exactly fit the container "most of the
1089 // time".
1091 // XXX Is this check really sufficient to catch all the incremental cases
1092 // where the ideal case doesn't have a scrollbar?
1093 if ((aState.mReflowedContentsWithHScrollbar ||
1094 aState.mReflowedContentsWithVScrollbar) &&
1095 aState.mVScrollbar != ShowScrollbar::Always &&
1096 aState.mHScrollbar != ShowScrollbar::Always) {
1097 nsSize kidSize = GetContainSizeAxes().ContainSize(
1098 kidDesiredSize.PhysicalSize(), *aState.mReflowInput.mFrame);
1099 nsSize insideBorderSize = ComputeInsideBorderSize(aState, kidSize);
1100 nsRect scrolledRect = GetUnsnappedScrolledRectInternal(
1101 kidDesiredSize.ScrollableOverflow(), insideBorderSize);
1102 if (nsRect(nsPoint(0, 0), insideBorderSize).Contains(scrolledRect)) {
1103 // Let's pretend we had no scrollbars coming in here
1104 kidDesiredSize.mOverflowAreas.Clear();
1105 ReflowScrolledFrame(aState, false, false, &kidDesiredSize);
1109 if (IsRootScrollFrameOfDocument()) {
1110 UpdateMinimumScaleSize(aState.mContentsOverflowAreas.ScrollableOverflow(),
1111 kidDesiredSize.PhysicalSize());
1114 // Try vertical scrollbar settings that leave the vertical scrollbar
1115 // unchanged. Do this first because changing the vertical scrollbar setting is
1116 // expensive, forcing a reflow always.
1118 // Try leaving the horizontal scrollbar unchanged first. This will be more
1119 // efficient.
1120 ROOT_SCROLLBAR_LOG("Trying layout1 with %d, %d\n",
1121 aState.mReflowedContentsWithHScrollbar,
1122 aState.mReflowedContentsWithVScrollbar);
1123 if (TryLayout(aState, &kidDesiredSize, aState.mReflowedContentsWithHScrollbar,
1124 aState.mReflowedContentsWithVScrollbar, false)) {
1125 return;
1127 ROOT_SCROLLBAR_LOG("Trying layout2 with %d, %d\n",
1128 !aState.mReflowedContentsWithHScrollbar,
1129 aState.mReflowedContentsWithVScrollbar);
1130 if (TryLayout(aState, &kidDesiredSize,
1131 !aState.mReflowedContentsWithHScrollbar,
1132 aState.mReflowedContentsWithVScrollbar, false)) {
1133 return;
1136 // OK, now try toggling the vertical scrollbar. The performance advantage
1137 // of trying the status-quo horizontal scrollbar state
1138 // does not exist here (we'll have to reflow due to the vertical scrollbar
1139 // change), so always try no horizontal scrollbar first.
1140 bool newVScrollbarState = !aState.mReflowedContentsWithVScrollbar;
1141 ROOT_SCROLLBAR_LOG("Trying layout3 with %d, %d\n", false, newVScrollbarState);
1142 if (TryLayout(aState, &kidDesiredSize, false, newVScrollbarState, false)) {
1143 return;
1145 ROOT_SCROLLBAR_LOG("Trying layout4 with %d, %d\n", true, newVScrollbarState);
1146 if (TryLayout(aState, &kidDesiredSize, true, newVScrollbarState, false)) {
1147 return;
1150 // OK, we're out of ideas. Try again enabling whatever scrollbars we can
1151 // enable and force the layout to stick even if it's inconsistent.
1152 // This just happens sometimes.
1153 ROOT_SCROLLBAR_LOG("Giving up, adding both scrollbars...\n");
1154 TryLayout(aState, &kidDesiredSize, aState.mHScrollbar != ShowScrollbar::Never,
1155 aState.mVScrollbar != ShowScrollbar::Never, true);
1158 void nsHTMLScrollFrame::PlaceScrollArea(ScrollReflowInput& aState,
1159 const nsPoint& aScrollPosition) {
1160 nsIFrame* scrolledFrame = mScrolledFrame;
1161 // Set the x,y of the scrolled frame to the correct value
1162 scrolledFrame->SetPosition(ScrollPort().TopLeft() - aScrollPosition);
1164 // Recompute our scrollable overflow, taking perspective children into
1165 // account. Note that this only recomputes the overflow areas stored on the
1166 // helper (which are used to compute scrollable length and scrollbar thumb
1167 // sizes) but not the overflow areas stored on the frame. This seems to work
1168 // for now, but it's possible that we may need to update both in the future.
1169 AdjustForPerspective(aState.mContentsOverflowAreas.ScrollableOverflow());
1171 // Preserve the width or height of empty rects
1172 const nsSize portSize = ScrollPort().Size();
1173 nsRect scrolledRect = GetUnsnappedScrolledRectInternal(
1174 aState.mContentsOverflowAreas.ScrollableOverflow(), portSize);
1175 nsRect scrolledArea =
1176 scrolledRect.UnionEdges(nsRect(nsPoint(0, 0), portSize));
1178 // Store the new overflow area. Note that this changes where an outline
1179 // of the scrolled frame would be painted, but scrolled frames can't have
1180 // outlines (the outline would go on this scrollframe instead).
1181 // Using FinishAndStoreOverflow is needed so the overflow rect gets set
1182 // correctly. It also messes with the overflow rect in the 'clip' case, but
1183 // scrolled frames can't have 'overflow' either.
1184 // This needs to happen before SyncFrameViewAfterReflow so
1185 // HasOverflowRect() will return the correct value.
1186 OverflowAreas overflow(scrolledArea, scrolledArea);
1187 scrolledFrame->FinishAndStoreOverflow(overflow, scrolledFrame->GetSize());
1189 // Note that making the view *exactly* the size of the scrolled area
1190 // is critical, since the view scrolling code uses the size of the
1191 // scrolled view to clamp scroll requests.
1192 // Normally the scrolledFrame won't have a view but in some cases it
1193 // might create its own.
1194 nsContainerFrame::SyncFrameViewAfterReflow(
1195 scrolledFrame->PresContext(), scrolledFrame, scrolledFrame->GetView(),
1196 scrolledArea, ReflowChildFlags::Default);
1199 nscoord nsHTMLScrollFrame::IntrinsicScrollbarGutterSizeAtInlineEdges(
1200 gfxContext* aRenderingContext) {
1201 const bool isVerticalWM = GetWritingMode().IsVertical();
1202 nsScrollbarFrame* inlineEndScrollbarBox =
1203 isVerticalWM ? mHScrollbarBox : mVScrollbarBox;
1204 if (!inlineEndScrollbarBox) {
1205 // No scrollbar box frame means no intrinsic size.
1206 return 0;
1209 if (PresContext()->UseOverlayScrollbars()) {
1210 return 0;
1213 const auto* styleForScrollbar = nsLayoutUtils::StyleForScrollbar(this);
1214 if (styleForScrollbar->StyleUIReset()->ScrollbarWidth() ==
1215 StyleScrollbarWidth::None) {
1216 // Scrollbar shouldn't appear at all with "scrollbar-width: none".
1217 return 0;
1220 const auto& styleScrollbarGutter =
1221 styleForScrollbar->StyleDisplay()->mScrollbarGutter;
1222 ScrollStyles ss = GetScrollStyles();
1223 const StyleOverflow& inlineEndStyleOverflow =
1224 isVerticalWM ? ss.mHorizontal : ss.mVertical;
1226 // Return the scrollbar-gutter size only if we have "overflow:scroll" or
1227 // non-auto "scrollbar-gutter", so early-return here if the conditions aren't
1228 // satisfied.
1229 if (inlineEndStyleOverflow != StyleOverflow::Scroll &&
1230 styleScrollbarGutter == StyleScrollbarGutter::AUTO) {
1231 return 0;
1234 // No need to worry about reflow depth here since it's just for scrollbars.
1235 nsSize scrollbarPrefSize = inlineEndScrollbarBox->ScrollbarMinSize();
1236 const nscoord scrollbarSize =
1237 isVerticalWM ? scrollbarPrefSize.height : scrollbarPrefSize.width;
1238 const auto bothEdges =
1239 bool(styleScrollbarGutter & StyleScrollbarGutter::BOTH_EDGES);
1240 return bothEdges ? scrollbarSize * 2 : scrollbarSize;
1243 // Legacy, this sucks!
1244 static bool IsMarqueeScrollbox(const nsIFrame& aScrollFrame) {
1245 if (!aScrollFrame.GetContent()) {
1246 return false;
1248 if (MOZ_LIKELY(!aScrollFrame.GetContent()->HasBeenInUAWidget())) {
1249 return false;
1251 MOZ_ASSERT(aScrollFrame.GetParent() &&
1252 aScrollFrame.GetParent()->GetContent());
1253 return aScrollFrame.GetParent() &&
1254 HTMLMarqueeElement::FromNodeOrNull(
1255 aScrollFrame.GetParent()->GetContent());
1258 /* virtual */
1259 nscoord nsHTMLScrollFrame::GetMinISize(gfxContext* aRenderingContext) {
1260 nscoord result = [&] {
1261 if (const Maybe<nscoord> containISize = ContainIntrinsicISize()) {
1262 return *containISize;
1264 if (MOZ_UNLIKELY(IsMarqueeScrollbox(*this))) {
1265 return 0;
1267 return mScrolledFrame->GetMinISize(aRenderingContext);
1268 }();
1270 DISPLAY_MIN_INLINE_SIZE(this, result);
1271 return result + IntrinsicScrollbarGutterSizeAtInlineEdges(aRenderingContext);
1274 /* virtual */
1275 nscoord nsHTMLScrollFrame::GetPrefISize(gfxContext* aRenderingContext) {
1276 const Maybe<nscoord> containISize = ContainIntrinsicISize();
1277 nscoord result = containISize
1278 ? *containISize
1279 : mScrolledFrame->GetPrefISize(aRenderingContext);
1280 DISPLAY_PREF_INLINE_SIZE(this, result);
1281 return NSCoordSaturatingAdd(
1282 result, IntrinsicScrollbarGutterSizeAtInlineEdges(aRenderingContext));
1285 // When we have perspective set on the outer scroll frame, and transformed
1286 // children (possibly with preserve-3d) then the effective transform on the
1287 // child depends on the offset to the scroll frame, which changes as we scroll.
1288 // This perspective transform can cause the element to move relative to the
1289 // scrolled inner frame, which would cause the scrollable length changes during
1290 // scrolling if we didn't account for it. Since we don't want scrollHeight/Width
1291 // and the size of scrollbar thumbs to change during scrolling, we compute the
1292 // scrollable overflow by determining the scroll position at which the child
1293 // becomes completely visible within the scrollport rather than using the union
1294 // of the overflow areas at their current position.
1295 static void GetScrollableOverflowForPerspective(
1296 nsIFrame* aScrolledFrame, nsIFrame* aCurrentFrame, const nsRect aScrollPort,
1297 nsPoint aOffset, nsRect& aScrolledFrameOverflowArea) {
1298 // Iterate over all children except pop-ups.
1299 for (const auto& [list, listID] : aCurrentFrame->ChildLists()) {
1300 if (listID == FrameChildListID::Popup) {
1301 continue;
1304 for (nsIFrame* child : list) {
1305 nsPoint offset = aOffset;
1307 // When we reach a direct child of the scroll, then we record the offset
1308 // to convert from that frame's coordinate into the scroll frame's
1309 // coordinates. Preserve-3d descendant frames use the same offset as their
1310 // ancestors, since TransformRect already converts us into the coordinate
1311 // space of the preserve-3d root.
1312 if (aScrolledFrame == aCurrentFrame) {
1313 offset = child->GetPosition();
1316 if (child->Extend3DContext()) {
1317 // If we're a preserve-3d frame, then recurse and include our
1318 // descendants since overflow of preserve-3d frames is only included
1319 // in the post-transform overflow area of the preserve-3d root frame.
1320 GetScrollableOverflowForPerspective(aScrolledFrame, child, aScrollPort,
1321 offset, aScrolledFrameOverflowArea);
1324 // If we're transformed, then we want to consider the possibility that
1325 // this frame might move relative to the scrolled frame when scrolling.
1326 // For preserve-3d, leaf frames have correct overflow rects relative to
1327 // themselves. preserve-3d 'nodes' (intermediate frames and the root) have
1328 // only their untransformed children included in their overflow relative
1329 // to self, which is what we want to include here.
1330 if (child->IsTransformed()) {
1331 // Compute the overflow rect for this leaf transform frame in the
1332 // coordinate space of the scrolled frame.
1333 nsPoint scrollPos = aScrolledFrame->GetPosition();
1334 nsRect preScroll, postScroll;
1336 // TODO: Can we reuse the reference box?
1337 TransformReferenceBox refBox(child);
1338 preScroll = nsDisplayTransform::TransformRect(
1339 child->ScrollableOverflowRectRelativeToSelf(), child, refBox);
1342 // Temporarily override the scroll position of the scrolled frame by
1343 // 10 CSS pixels, and then recompute what the overflow rect would be.
1344 // This scroll position may not be valid, but that shouldn't matter
1345 // for our calculations.
1347 aScrolledFrame->SetPosition(scrollPos + nsPoint(600, 600));
1348 TransformReferenceBox refBox(child);
1349 postScroll = nsDisplayTransform::TransformRect(
1350 child->ScrollableOverflowRectRelativeToSelf(), child, refBox);
1351 aScrolledFrame->SetPosition(scrollPos);
1354 // Compute how many app units the overflow rects moves by when we adjust
1355 // the scroll position by 1 app unit.
1356 double rightDelta =
1357 (postScroll.XMost() - preScroll.XMost() + 600.0) / 600.0;
1358 double bottomDelta =
1359 (postScroll.YMost() - preScroll.YMost() + 600.0) / 600.0;
1361 // We can't ever have negative scrolling.
1362 NS_ASSERTION(rightDelta > 0.0f && bottomDelta > 0.0f,
1363 "Scrolling can't be reversed!");
1365 // Move preScroll into the coordinate space of the scrollport.
1366 preScroll += offset + scrollPos;
1368 // For each of the four edges of preScroll, figure out how far they
1369 // extend beyond the scrollport. Ignore negative values since that means
1370 // that side is already scrolled in to view and we don't need to add
1371 // overflow to account for it.
1372 nsMargin overhang(std::max(0, aScrollPort.Y() - preScroll.Y()),
1373 std::max(0, preScroll.XMost() - aScrollPort.XMost()),
1374 std::max(0, preScroll.YMost() - aScrollPort.YMost()),
1375 std::max(0, aScrollPort.X() - preScroll.X()));
1377 // Scale according to rightDelta/bottomDelta to adjust for the different
1378 // scroll rates.
1379 overhang.top = NSCoordSaturatingMultiply(
1380 overhang.top, static_cast<float>(1 / bottomDelta));
1381 overhang.right = NSCoordSaturatingMultiply(
1382 overhang.right, static_cast<float>(1 / rightDelta));
1383 overhang.bottom = NSCoordSaturatingMultiply(
1384 overhang.bottom, static_cast<float>(1 / bottomDelta));
1385 overhang.left = NSCoordSaturatingMultiply(
1386 overhang.left, static_cast<float>(1 / rightDelta));
1388 // Take the minimum overflow rect that would allow the current scroll
1389 // position, using the size of the scroll port and offset by the
1390 // inverse of the scroll position.
1391 nsRect overflow = aScrollPort - scrollPos;
1393 // Expand it by our margins to get an overflow rect that would allow all
1394 // edges of our transformed content to be scrolled into view.
1395 overflow.Inflate(overhang);
1397 // Merge it with the combined overflow
1398 aScrolledFrameOverflowArea.UnionRect(aScrolledFrameOverflowArea,
1399 overflow);
1400 } else if (aCurrentFrame == aScrolledFrame) {
1401 aScrolledFrameOverflowArea.UnionRect(
1402 aScrolledFrameOverflowArea,
1403 child->ScrollableOverflowRectRelativeToParent());
1409 BaselineSharingGroup nsHTMLScrollFrame::GetDefaultBaselineSharingGroup() const {
1410 return mScrolledFrame->GetDefaultBaselineSharingGroup();
1413 nscoord nsHTMLScrollFrame::SynthesizeFallbackBaseline(
1414 mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup) const {
1415 // Marign-end even for central baselines.
1416 if (aWM.IsLineInverted()) {
1417 return -GetLogicalUsedMargin(aWM).BStart(aWM);
1419 return aBaselineGroup == BaselineSharingGroup::First
1420 ? BSize(aWM) + GetLogicalUsedMargin(aWM).BEnd(aWM)
1421 : -GetLogicalUsedMargin(aWM).BEnd(aWM);
1424 Maybe<nscoord> nsHTMLScrollFrame::GetNaturalBaselineBOffset(
1425 WritingMode aWM, BaselineSharingGroup aBaselineGroup,
1426 BaselineExportContext aExportContext) const {
1427 // Block containers that are scrollable always have a last baseline
1428 // that are synthesized from block-end margin edge.
1429 // Note(dshin): This behaviour is really only relevant to `inline-block`
1430 // alignment context. In the context of table/flex/grid alignment, first/last
1431 // baselines are calculated through `GetFirstLineBaseline`, which does
1432 // calculations of its own.
1433 // https://drafts.csswg.org/css-align/#baseline-export
1434 if (aExportContext == BaselineExportContext::LineLayout &&
1435 aBaselineGroup == BaselineSharingGroup::Last &&
1436 mScrolledFrame->IsBlockFrameOrSubclass()) {
1437 return Some(SynthesizeFallbackBaseline(aWM, aBaselineGroup));
1440 if (StyleDisplay()->IsContainLayout()) {
1441 return Nothing{};
1444 // OK, here's where we defer to our scrolled frame.
1445 return mScrolledFrame
1446 ->GetNaturalBaselineBOffset(aWM, aBaselineGroup, aExportContext)
1447 .map([this, aWM](nscoord aBaseline) {
1448 // We have to add our border BStart thickness to whatever it returns, to
1449 // produce an offset in our frame-rect's coordinate system. (We don't
1450 // have to add padding, because the scrolled frame handles our padding.)
1451 LogicalMargin border = GetLogicalUsedBorder(aWM);
1452 const auto bSize = GetLogicalSize(aWM).BSize(aWM);
1453 // Clamp the baseline to the border rect. See bug 1791069.
1454 return std::clamp(border.BStart(aWM) + aBaseline, 0, bSize);
1458 void nsHTMLScrollFrame::AdjustForPerspective(nsRect& aScrollableOverflow) {
1459 // If we have perspective that is being applied to our children, then
1460 // the effective transform on the child depends on the relative position
1461 // of the child to us and changes during scrolling.
1462 if (!ChildrenHavePerspective()) {
1463 return;
1465 aScrollableOverflow.SetEmpty();
1466 GetScrollableOverflowForPerspective(mScrolledFrame, mScrolledFrame,
1467 ScrollPort(), nsPoint(),
1468 aScrollableOverflow);
1471 void nsHTMLScrollFrame::Reflow(nsPresContext* aPresContext,
1472 ReflowOutput& aDesiredSize,
1473 const ReflowInput& aReflowInput,
1474 nsReflowStatus& aStatus) {
1475 MarkInReflow();
1476 DO_GLOBAL_REFLOW_COUNT("nsHTMLScrollFrame");
1477 DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
1478 MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
1480 HandleScrollbarStyleSwitching();
1482 ScrollReflowInput state(this, aReflowInput);
1484 //------------ Handle Incremental Reflow -----------------
1485 bool reflowHScrollbar = true;
1486 bool reflowVScrollbar = true;
1487 bool reflowScrollCorner = true;
1488 if (!aReflowInput.ShouldReflowAllKids()) {
1489 auto NeedsReflow = [](const nsIFrame* aFrame) {
1490 return aFrame && aFrame->IsSubtreeDirty();
1493 reflowHScrollbar = NeedsReflow(mHScrollbarBox);
1494 reflowVScrollbar = NeedsReflow(mVScrollbarBox);
1495 reflowScrollCorner =
1496 NeedsReflow(mScrollCornerBox) || NeedsReflow(mResizerBox);
1499 if (mIsRoot) {
1500 reflowScrollCorner = false;
1503 const nsRect oldScrollPort = ScrollPort();
1504 nsRect oldScrolledAreaBounds =
1505 mScrolledFrame->ScrollableOverflowRectRelativeToParent();
1506 nsPoint oldScrollPosition = GetScrollPosition();
1508 ReflowContents(state, aDesiredSize);
1510 nsSize layoutSize =
1511 mIsUsingMinimumScaleSize ? mMinimumScaleSize : state.mInsideBorderSize;
1512 aDesiredSize.Width() = layoutSize.width + state.mComputedBorder.LeftRight();
1513 aDesiredSize.Height() = layoutSize.height + state.mComputedBorder.TopBottom();
1515 // Set the size of the frame now since computing the perspective-correct
1516 // overflow (within PlaceScrollArea) can rely on it.
1517 SetSize(aDesiredSize.GetWritingMode(),
1518 aDesiredSize.Size(aDesiredSize.GetWritingMode()));
1520 // Restore the old scroll position, for now, even if that's not valid anymore
1521 // because we changed size. We'll fix it up in a post-reflow callback, because
1522 // our current size may only be temporary (e.g. we're compute XUL desired
1523 // sizes).
1524 PlaceScrollArea(state, oldScrollPosition);
1525 if (!mPostedReflowCallback) {
1526 // Make sure we'll try scrolling to restored position
1527 PresShell()->PostReflowCallback(this);
1528 mPostedReflowCallback = true;
1531 bool didOnlyHScrollbar = mOnlyNeedHScrollbarToScrollVVInsideLV;
1532 bool didOnlyVScrollbar = mOnlyNeedVScrollbarToScrollVVInsideLV;
1533 mOnlyNeedHScrollbarToScrollVVInsideLV =
1534 state.mOnlyNeedHScrollbarToScrollVVInsideLV;
1535 mOnlyNeedVScrollbarToScrollVVInsideLV =
1536 state.mOnlyNeedVScrollbarToScrollVVInsideLV;
1538 bool didHaveHScrollbar = mHasHorizontalScrollbar;
1539 bool didHaveVScrollbar = mHasVerticalScrollbar;
1540 mHasHorizontalScrollbar = state.mShowHScrollbar;
1541 mHasVerticalScrollbar = state.mShowVScrollbar;
1542 const nsRect& newScrollPort = ScrollPort();
1543 nsRect newScrolledAreaBounds =
1544 mScrolledFrame->ScrollableOverflowRectRelativeToParent();
1545 if (mSkippedScrollbarLayout || reflowHScrollbar || reflowVScrollbar ||
1546 reflowScrollCorner || HasAnyStateBits(NS_FRAME_IS_DIRTY) ||
1547 didHaveHScrollbar != state.mShowHScrollbar ||
1548 didHaveVScrollbar != state.mShowVScrollbar ||
1549 didOnlyHScrollbar != mOnlyNeedHScrollbarToScrollVVInsideLV ||
1550 didOnlyVScrollbar != mOnlyNeedVScrollbarToScrollVVInsideLV ||
1551 !oldScrollPort.IsEqualEdges(newScrollPort) ||
1552 !oldScrolledAreaBounds.IsEqualEdges(newScrolledAreaBounds)) {
1553 if (!mSuppressScrollbarUpdate) {
1554 mSkippedScrollbarLayout = false;
1555 nsHTMLScrollFrame::SetScrollbarVisibility(mHScrollbarBox,
1556 state.mShowHScrollbar);
1557 nsHTMLScrollFrame::SetScrollbarVisibility(mVScrollbarBox,
1558 state.mShowVScrollbar);
1559 // place and reflow scrollbars
1560 const nsRect insideBorderArea(
1561 nsPoint(state.mComputedBorder.left, state.mComputedBorder.top),
1562 layoutSize);
1563 LayoutScrollbars(state, insideBorderArea, oldScrollPort);
1564 } else {
1565 mSkippedScrollbarLayout = true;
1568 if (mIsRoot) {
1569 if (RefPtr<MobileViewportManager> manager =
1570 PresShell()->GetMobileViewportManager()) {
1571 // Note that this runs during layout, and when we get here the root
1572 // scrollframe has already been laid out. It may have added or removed
1573 // scrollbars as a result of that layout, so we need to ensure the
1574 // visual viewport is updated to account for that before we read the
1575 // visual viewport size.
1576 manager->UpdateVisualViewportSizeForPotentialScrollbarChange();
1577 } else if (oldScrollPort.Size() != newScrollPort.Size()) {
1578 // We want to make sure to send a visual viewport resize event if the
1579 // scrollport changed sizes for root scroll frames. The
1580 // MobileViewportManager will do that, but if we don't have one (ie we
1581 // aren't a root content document for example) we have to send one
1582 // ourselves.
1583 if (auto* window = nsGlobalWindowInner::Cast(
1584 aPresContext->Document()->GetInnerWindow())) {
1585 window->VisualViewport()->PostResizeEvent();
1590 // Note that we need to do this after the
1591 // UpdateVisualViewportSizeForPotentialScrollbarChange call above because that
1592 // is what updates the visual viewport size and we need it to be up to date.
1593 if (mIsRoot && !state.OverlayScrollbars() &&
1594 (didHaveHScrollbar != state.mShowHScrollbar ||
1595 didHaveVScrollbar != state.mShowVScrollbar ||
1596 didOnlyHScrollbar != mOnlyNeedHScrollbarToScrollVVInsideLV ||
1597 didOnlyVScrollbar != mOnlyNeedVScrollbarToScrollVVInsideLV) &&
1598 PresShell()->IsVisualViewportOffsetSet()) {
1599 // Removing layout/classic scrollbars can make a previously valid vvoffset
1600 // invalid. For example, if we are zoomed in on an overflow hidden document
1601 // and then zoom back out, when apz reaches the initial resolution (ie 1.0)
1602 // it won't know that we can remove the scrollbars, so the vvoffset can
1603 // validly be upto the width/height of the scrollbars. After we reflow and
1604 // remove the scrollbars the only valid vvoffset is (0,0). We could wait
1605 // until we send the new frame metrics to apz and then have it reply with
1606 // the new corrected vvoffset but having an inconsistent vvoffset causes
1607 // problems so trigger the vvoffset to be re-set and re-clamped in
1608 // ReflowFinished.
1609 mReclampVVOffsetInReflowFinished = true;
1612 aDesiredSize.SetOverflowAreasToDesiredBounds();
1614 UpdateSticky();
1615 FinishReflowWithAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput,
1616 aStatus);
1618 if (!InInitialReflow() && !mHadNonInitialReflow) {
1619 mHadNonInitialReflow = true;
1622 if (mIsRoot && !oldScrolledAreaBounds.IsEqualEdges(newScrolledAreaBounds)) {
1623 PostScrolledAreaEvent();
1626 UpdatePrevScrolledRect();
1628 aStatus.Reset(); // This type of frame can't be split.
1629 PostOverflowEvent();
1632 void nsHTMLScrollFrame::DidReflow(nsPresContext* aPresContext,
1633 const ReflowInput* aReflowInput) {
1634 nsContainerFrame::DidReflow(aPresContext, aReflowInput);
1635 if (NeedsResnap()) {
1636 PostPendingResnap();
1637 } else {
1638 PresShell()->PostPendingScrollAnchorAdjustment(Anchor());
1642 ////////////////////////////////////////////////////////////////////////////////
1644 #ifdef DEBUG_FRAME_DUMP
1645 nsresult nsHTMLScrollFrame::GetFrameName(nsAString& aResult) const {
1646 return MakeFrameName(u"HTMLScroll"_ns, aResult);
1648 #endif
1650 #ifdef ACCESSIBILITY
1651 a11y::AccType nsHTMLScrollFrame::AccessibleType() {
1652 if (IsTableCaption()) {
1653 return GetRect().IsEmpty() ? a11y::eNoType : a11y::eHTMLCaptionType;
1656 // Create an accessible regardless of focusable state because the state can be
1657 // changed during frame life cycle without any notifications to accessibility.
1658 if (mContent->IsRootOfNativeAnonymousSubtree() ||
1659 GetScrollStyles().IsHiddenInBothDirections()) {
1660 return a11y::eNoType;
1663 return a11y::eHyperTextType;
1665 #endif
1667 NS_QUERYFRAME_HEAD(nsHTMLScrollFrame)
1668 NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
1669 NS_QUERYFRAME_ENTRY(nsIScrollableFrame)
1670 NS_QUERYFRAME_ENTRY(nsIStatefulFrame)
1671 NS_QUERYFRAME_ENTRY(nsIScrollbarMediator)
1672 NS_QUERYFRAME_ENTRY(nsHTMLScrollFrame)
1673 NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
1675 nsMargin nsHTMLScrollFrame::GetDesiredScrollbarSizes() const {
1676 nsPresContext* pc = PresContext();
1677 if (pc->UseOverlayScrollbars()) {
1678 return {};
1681 const auto& style = *nsLayoutUtils::StyleForScrollbar(this);
1682 const auto scrollbarWidth = style.StyleUIReset()->ScrollbarWidth();
1683 if (scrollbarWidth == StyleScrollbarWidth::None) {
1684 return {};
1687 ScrollStyles styles = GetScrollStyles();
1688 nsMargin result(0, 0, 0, 0);
1690 auto size = GetNonOverlayScrollbarSize(pc, scrollbarWidth);
1691 if (styles.mVertical != StyleOverflow::Hidden) {
1692 if (IsScrollbarOnRight()) {
1693 result.left = size;
1694 } else {
1695 result.right = size;
1699 if (styles.mHorizontal != StyleOverflow::Hidden) {
1700 // We don't currently support any scripts that would require a scrollbar
1701 // at the top. (Are there any?)
1702 result.bottom = size;
1705 return result;
1708 nscoord nsHTMLScrollFrame::GetNonOverlayScrollbarSize(
1709 const nsPresContext* aPc, StyleScrollbarWidth aScrollbarWidth) {
1710 const auto size = aPc->Theme()->GetScrollbarSize(aPc, aScrollbarWidth,
1711 nsITheme::Overlay::No);
1712 return aPc->DevPixelsToAppUnits(size);
1715 void nsHTMLScrollFrame::HandleScrollbarStyleSwitching() {
1716 // Check if we switched between scrollbar styles.
1717 if (mScrollbarActivity && !UsesOverlayScrollbars()) {
1718 mScrollbarActivity->Destroy();
1719 mScrollbarActivity = nullptr;
1720 } else if (!mScrollbarActivity && UsesOverlayScrollbars()) {
1721 mScrollbarActivity = new ScrollbarActivity(do_QueryFrame(this));
1725 #if defined(MOZ_WIDGET_ANDROID)
1726 static bool IsFocused(nsIContent* aContent) {
1727 // Some content elements, like the GetContent() of a scroll frame
1728 // for a text input field, are inside anonymous subtrees, but the focus
1729 // manager always reports a non-anonymous element as the focused one, so
1730 // walk up the tree until we reach a non-anonymous element.
1731 while (aContent && aContent->IsInNativeAnonymousSubtree()) {
1732 aContent = aContent->GetParent();
1735 return aContent ? nsContentUtils::IsFocusedContent(aContent) : false;
1737 #endif
1739 void nsHTMLScrollFrame::SetScrollableByAPZ(bool aScrollable) {
1740 mScrollableByAPZ = aScrollable;
1743 void nsHTMLScrollFrame::SetZoomableByAPZ(bool aZoomable) {
1744 if (!nsLayoutUtils::UsesAsyncScrolling(this)) {
1745 // If APZ is disabled on this window, then we're never actually going to
1746 // do any zooming. So we don't need to do any of the setup for it. Note
1747 // that this function gets called from ZoomConstraintsClient even if APZ
1748 // is disabled to indicate the zoomability of content.
1749 aZoomable = false;
1751 if (mZoomableByAPZ != aZoomable) {
1752 // We might be changing the result of DecideScrollableLayer() so schedule a
1753 // paint to make sure we pick up the result of that change.
1754 mZoomableByAPZ = aZoomable;
1755 SchedulePaint();
1759 void nsHTMLScrollFrame::SetHasOutOfFlowContentInsideFilter() {
1760 mHasOutOfFlowContentInsideFilter = true;
1763 bool nsHTMLScrollFrame::WantAsyncScroll() const {
1764 ScrollStyles styles = GetScrollStyles();
1766 // First, as an optimization because getting the scrollrange is
1767 // relatively slow, check overflow hidden and not a zoomed scroll frame.
1768 if (styles.mHorizontal == StyleOverflow::Hidden &&
1769 styles.mVertical == StyleOverflow::Hidden) {
1770 if (!mIsRoot || GetVisualViewportSize() == mScrollPort.Size()) {
1771 return false;
1775 nscoord oneDevPixel =
1776 GetScrolledFrame()->PresContext()->AppUnitsPerDevPixel();
1777 nsRect scrollRange = GetLayoutScrollRange();
1779 bool isVScrollable = (scrollRange.height >= oneDevPixel) &&
1780 (styles.mVertical != StyleOverflow::Hidden);
1781 bool isHScrollable = (scrollRange.width >= oneDevPixel) &&
1782 (styles.mHorizontal != StyleOverflow::Hidden);
1784 #if defined(MOZ_WIDGET_ANDROID)
1785 // Mobile platforms need focus to scroll text inputs.
1786 bool canScrollWithoutScrollbars =
1787 !IsForTextControlWithNoScrollbars() || IsFocused(GetContent());
1788 #else
1789 bool canScrollWithoutScrollbars = true;
1790 #endif
1792 // The check for scroll bars was added in bug 825692 to prevent layerization
1793 // of text inputs for performance reasons.
1794 bool isVAsyncScrollable =
1795 isVScrollable && (mVScrollbarBox || canScrollWithoutScrollbars);
1796 bool isHAsyncScrollable =
1797 isHScrollable && (mHScrollbarBox || canScrollWithoutScrollbars);
1798 if (isVAsyncScrollable || isHAsyncScrollable) {
1799 return true;
1802 // If the page has a visual viewport size that's different from
1803 // the layout viewport size at the current zoom level, we need to be
1804 // able to scroll the visual viewport inside the layout viewport
1805 // even if the page is not zoomable.
1806 return mIsRoot && GetVisualViewportSize() != mScrollPort.Size() &&
1807 !GetVisualScrollRange().IsEqualInterior(scrollRange);
1810 static nsRect GetOnePixelRangeAroundPoint(const nsPoint& aPoint,
1811 bool aIsHorizontal) {
1812 nsRect allowedRange(aPoint, nsSize());
1813 nscoord halfPixel = nsPresContext::CSSPixelsToAppUnits(0.5f);
1814 if (aIsHorizontal) {
1815 allowedRange.x = aPoint.x - halfPixel;
1816 allowedRange.width = halfPixel * 2 - 1;
1817 } else {
1818 allowedRange.y = aPoint.y - halfPixel;
1819 allowedRange.height = halfPixel * 2 - 1;
1821 return allowedRange;
1824 void nsHTMLScrollFrame::ScrollByPage(nsScrollbarFrame* aScrollbar,
1825 int32_t aDirection,
1826 ScrollSnapFlags aSnapFlags) {
1827 ScrollByUnit(aScrollbar, ScrollMode::Smooth, aDirection, ScrollUnit::PAGES,
1828 aSnapFlags);
1831 void nsHTMLScrollFrame::ScrollByWhole(nsScrollbarFrame* aScrollbar,
1832 int32_t aDirection,
1833 ScrollSnapFlags aSnapFlags) {
1834 ScrollByUnit(aScrollbar, ScrollMode::Instant, aDirection, ScrollUnit::WHOLE,
1835 aSnapFlags);
1838 void nsHTMLScrollFrame::ScrollByLine(nsScrollbarFrame* aScrollbar,
1839 int32_t aDirection,
1840 ScrollSnapFlags aSnapFlags) {
1841 bool isHorizontal = aScrollbar->IsHorizontal();
1842 nsIntPoint delta;
1843 if (isHorizontal) {
1844 const double kScrollMultiplier =
1845 StaticPrefs::toolkit_scrollbox_horizontalScrollDistance();
1846 delta.x = static_cast<int32_t>(aDirection * kScrollMultiplier);
1847 if (GetLineScrollAmount().width * delta.x > GetPageScrollAmount().width) {
1848 // The scroll frame is so small that the delta would be more
1849 // than an entire page. Scroll by one page instead to maintain
1850 // context.
1851 ScrollByPage(aScrollbar, aDirection);
1852 return;
1854 } else {
1855 const double kScrollMultiplier =
1856 StaticPrefs::toolkit_scrollbox_verticalScrollDistance();
1857 delta.y = static_cast<int32_t>(aDirection * kScrollMultiplier);
1858 if (GetLineScrollAmount().height * delta.y > GetPageScrollAmount().height) {
1859 // The scroll frame is so small that the delta would be more
1860 // than an entire page. Scroll by one page instead to maintain
1861 // context.
1862 ScrollByPage(aScrollbar, aDirection);
1863 return;
1867 nsIntPoint overflow;
1868 ScrollBy(delta, ScrollUnit::LINES, ScrollMode::Smooth, &overflow,
1869 ScrollOrigin::Other, nsIScrollableFrame::NOT_MOMENTUM, aSnapFlags);
1872 void nsHTMLScrollFrame::RepeatButtonScroll(nsScrollbarFrame* aScrollbar) {
1873 aScrollbar->MoveToNewPosition(nsScrollbarFrame::ImplementsScrollByUnit::Yes);
1876 void nsHTMLScrollFrame::ThumbMoved(nsScrollbarFrame* aScrollbar,
1877 nscoord aOldPos, nscoord aNewPos) {
1878 MOZ_ASSERT(aScrollbar != nullptr);
1879 bool isHorizontal = aScrollbar->IsHorizontal();
1880 nsPoint current = GetScrollPosition();
1881 nsPoint dest = current;
1882 if (isHorizontal) {
1883 dest.x = IsPhysicalLTR() ? aNewPos : aNewPos - GetLayoutScrollRange().width;
1884 } else {
1885 dest.y = aNewPos;
1887 nsRect allowedRange = GetOnePixelRangeAroundPoint(dest, isHorizontal);
1889 // Don't try to scroll if we're already at an acceptable place.
1890 // Don't call Contains here since Contains returns false when the point is
1891 // on the bottom or right edge of the rectangle.
1892 if (allowedRange.ClampPoint(current) == current) {
1893 return;
1896 ScrollToWithOrigin(
1897 dest, &allowedRange,
1898 ScrollOperationParams{ScrollMode::Instant, ScrollOrigin::Other});
1901 void nsHTMLScrollFrame::ScrollbarReleased(nsScrollbarFrame* aScrollbar) {
1902 // Scrollbar scrolling does not result in fling gestures, clear any
1903 // accumulated velocity
1904 mVelocityQueue.Reset();
1906 // Perform scroll snapping, if needed. Scrollbar movement uses the same
1907 // smooth scrolling animation as keyboard scrolling.
1908 ScrollSnap(mDestination, ScrollMode::Smooth);
1911 void nsHTMLScrollFrame::ScrollByUnit(nsScrollbarFrame* aScrollbar,
1912 ScrollMode aMode, int32_t aDirection,
1913 ScrollUnit aUnit,
1914 ScrollSnapFlags aSnapFlags) {
1915 MOZ_ASSERT(aScrollbar != nullptr);
1916 bool isHorizontal = aScrollbar->IsHorizontal();
1917 nsIntPoint delta;
1918 if (isHorizontal) {
1919 delta.x = aDirection;
1920 } else {
1921 delta.y = aDirection;
1923 nsIntPoint overflow;
1924 ScrollBy(delta, aUnit, aMode, &overflow, ScrollOrigin::Other,
1925 nsIScrollableFrame::NOT_MOMENTUM, aSnapFlags);
1928 //-------------------- Helper ----------------------
1930 // AsyncSmoothMSDScroll has ref counting.
1931 class nsHTMLScrollFrame::AsyncSmoothMSDScroll final
1932 : public nsARefreshObserver {
1933 public:
1934 AsyncSmoothMSDScroll(const nsPoint& aInitialPosition,
1935 const nsPoint& aInitialDestination,
1936 const nsSize& aInitialVelocity, const nsRect& aRange,
1937 const mozilla::TimeStamp& aStartTime,
1938 nsPresContext* aPresContext,
1939 UniquePtr<ScrollSnapTargetIds> aSnapTargetIds,
1940 ScrollTriggeredByScript aTriggeredByScript)
1941 : mXAxisModel(aInitialPosition.x, aInitialDestination.x,
1942 aInitialVelocity.width,
1943 StaticPrefs::layout_css_scroll_behavior_spring_constant(),
1944 StaticPrefs::layout_css_scroll_behavior_damping_ratio()),
1945 mYAxisModel(aInitialPosition.y, aInitialDestination.y,
1946 aInitialVelocity.height,
1947 StaticPrefs::layout_css_scroll_behavior_spring_constant(),
1948 StaticPrefs::layout_css_scroll_behavior_damping_ratio()),
1949 mRange(aRange),
1950 mStartPosition(aInitialPosition),
1951 mLastRefreshTime(aStartTime),
1952 mCallee(nullptr),
1953 mOneDevicePixelInAppUnits(aPresContext->DevPixelsToAppUnits(1)),
1954 mSnapTargetIds(std::move(aSnapTargetIds)),
1955 mTriggeredByScript(aTriggeredByScript) {
1956 Telemetry::SetHistogramRecordingEnabled(
1957 Telemetry::FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS, true);
1960 NS_INLINE_DECL_REFCOUNTING(AsyncSmoothMSDScroll, override)
1962 nsSize GetVelocity() {
1963 // In nscoords per second
1964 return nsSize(mXAxisModel.GetVelocity(), mYAxisModel.GetVelocity());
1967 nsPoint GetPosition() {
1968 // In nscoords
1969 return nsPoint(NSToCoordRound(mXAxisModel.GetPosition()),
1970 NSToCoordRound(mYAxisModel.GetPosition()));
1973 void SetDestination(const nsPoint& aDestination,
1974 ScrollTriggeredByScript aTriggeredByScript) {
1975 mXAxisModel.SetDestination(static_cast<int32_t>(aDestination.x));
1976 mYAxisModel.SetDestination(static_cast<int32_t>(aDestination.y));
1977 mTriggeredByScript = aTriggeredByScript;
1980 void SetRange(const nsRect& aRange) { mRange = aRange; }
1982 nsRect GetRange() { return mRange; }
1984 nsPoint GetStartPosition() { return mStartPosition; }
1986 void Simulate(const TimeDuration& aDeltaTime) {
1987 mXAxisModel.Simulate(aDeltaTime);
1988 mYAxisModel.Simulate(aDeltaTime);
1990 nsPoint desired = GetPosition();
1991 nsPoint clamped = mRange.ClampPoint(desired);
1992 if (desired.x != clamped.x) {
1993 // The scroll has hit the "wall" at the left or right edge of the allowed
1994 // scroll range.
1995 // Absorb the impact to avoid bounceback effect.
1996 mXAxisModel.SetVelocity(0.0);
1997 mXAxisModel.SetPosition(clamped.x);
2000 if (desired.y != clamped.y) {
2001 // The scroll has hit the "wall" at the left or right edge of the allowed
2002 // scroll range.
2003 // Absorb the impact to avoid bounceback effect.
2004 mYAxisModel.SetVelocity(0.0);
2005 mYAxisModel.SetPosition(clamped.y);
2009 bool IsFinished() {
2010 return mXAxisModel.IsFinished(mOneDevicePixelInAppUnits) &&
2011 mYAxisModel.IsFinished(mOneDevicePixelInAppUnits);
2014 virtual void WillRefresh(mozilla::TimeStamp aTime) override {
2015 mozilla::TimeDuration deltaTime = aTime - mLastRefreshTime;
2016 mLastRefreshTime = aTime;
2018 // The callback may release "this".
2019 // We don't access members after returning, so no need for KungFuDeathGrip.
2020 nsHTMLScrollFrame::AsyncSmoothMSDScrollCallback(mCallee, deltaTime);
2024 * Set a refresh observer for smooth scroll iterations (and start observing).
2025 * Should be used at most once during the lifetime of this object.
2027 void SetRefreshObserver(nsHTMLScrollFrame* aCallee) {
2028 NS_ASSERTION(aCallee && !mCallee,
2029 "AsyncSmoothMSDScroll::SetRefreshObserver - Invalid usage.");
2031 RefreshDriver(aCallee)->AddRefreshObserver(this, FlushType::Style,
2032 "Smooth scroll (MSD) animation");
2033 mCallee = aCallee;
2037 * The mCallee holds a strong ref to us since the refresh driver doesn't.
2038 * Our dtor and mCallee's Destroy() method both call RemoveObserver() -
2039 * whichever comes first removes us from the refresh driver.
2041 void RemoveObserver() {
2042 if (mCallee) {
2043 RefreshDriver(mCallee)->RemoveRefreshObserver(this, FlushType::Style);
2044 mCallee = nullptr;
2048 UniquePtr<ScrollSnapTargetIds> TakeSnapTargetIds() {
2049 return std::move(mSnapTargetIds);
2052 bool WasTriggeredByScript() const {
2053 return mTriggeredByScript == ScrollTriggeredByScript::Yes;
2056 private:
2057 // Private destructor, to discourage deletion outside of Release():
2058 ~AsyncSmoothMSDScroll() {
2059 RemoveObserver();
2060 Telemetry::SetHistogramRecordingEnabled(
2061 Telemetry::FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS, false);
2064 nsRefreshDriver* RefreshDriver(nsHTMLScrollFrame* aCallee) {
2065 return aCallee->PresContext()->RefreshDriver();
2068 mozilla::layers::AxisPhysicsMSDModel mXAxisModel, mYAxisModel;
2069 nsRect mRange;
2070 nsPoint mStartPosition;
2071 mozilla::TimeStamp mLastRefreshTime;
2072 nsHTMLScrollFrame* mCallee;
2073 nscoord mOneDevicePixelInAppUnits;
2074 UniquePtr<ScrollSnapTargetIds> mSnapTargetIds;
2075 ScrollTriggeredByScript mTriggeredByScript;
2078 // AsyncScroll has ref counting.
2079 class nsHTMLScrollFrame::AsyncScroll final : public nsARefreshObserver {
2080 public:
2081 typedef mozilla::TimeStamp TimeStamp;
2082 typedef mozilla::TimeDuration TimeDuration;
2084 explicit AsyncScroll(UniquePtr<ScrollSnapTargetIds> aSnapTargetIds,
2085 ScrollTriggeredByScript aTriggeredByScript)
2086 : mOrigin(ScrollOrigin::NotSpecified),
2087 mCallee(nullptr),
2088 mSnapTargetIds(std::move(aSnapTargetIds)),
2089 mTriggeredByScript(aTriggeredByScript) {
2090 Telemetry::SetHistogramRecordingEnabled(
2091 Telemetry::FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS, true);
2094 private:
2095 // Private destructor, to discourage deletion outside of Release():
2096 ~AsyncScroll() {
2097 RemoveObserver();
2098 Telemetry::SetHistogramRecordingEnabled(
2099 Telemetry::FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS, false);
2102 public:
2103 void InitSmoothScroll(TimeStamp aTime, nsPoint aInitialPosition,
2104 nsPoint aDestination, ScrollOrigin aOrigin,
2105 const nsRect& aRange, const nsSize& aCurrentVelocity);
2106 void Init(nsPoint aInitialPosition, const nsRect& aRange) {
2107 mAnimationPhysics = nullptr;
2108 mRange = aRange;
2109 mStartPosition = aInitialPosition;
2112 bool IsSmoothScroll() { return mAnimationPhysics != nullptr; }
2114 bool IsFinished(const TimeStamp& aTime) const {
2115 MOZ_RELEASE_ASSERT(mAnimationPhysics);
2116 return mAnimationPhysics->IsFinished(aTime);
2119 nsPoint PositionAt(const TimeStamp& aTime) const {
2120 MOZ_RELEASE_ASSERT(mAnimationPhysics);
2121 return mAnimationPhysics->PositionAt(aTime);
2124 nsSize VelocityAt(const TimeStamp& aTime) const {
2125 MOZ_RELEASE_ASSERT(mAnimationPhysics);
2126 return mAnimationPhysics->VelocityAt(aTime);
2129 nsPoint GetStartPosition() const { return mStartPosition; }
2131 // Most recent scroll origin.
2132 ScrollOrigin mOrigin;
2134 // Allowed destination positions around mDestination
2135 nsRect mRange;
2137 // Initial position where the async scroll was triggered.
2138 nsPoint mStartPosition;
2140 private:
2141 void InitPreferences(TimeStamp aTime, nsAtom* aOrigin);
2143 UniquePtr<ScrollAnimationPhysics> mAnimationPhysics;
2145 // The next section is observer/callback management
2146 // Bodies of WillRefresh and RefreshDriver contain nsHTMLScrollFrame specific
2147 // code.
2148 public:
2149 NS_INLINE_DECL_REFCOUNTING(AsyncScroll, override)
2152 * Set a refresh observer for smooth scroll iterations (and start observing).
2153 * Should be used at most once during the lifetime of this object.
2155 void SetRefreshObserver(nsHTMLScrollFrame* aCallee) {
2156 NS_ASSERTION(aCallee && !mCallee,
2157 "AsyncScroll::SetRefreshObserver - Invalid usage.");
2159 RefreshDriver(aCallee)->AddRefreshObserver(this, FlushType::Style,
2160 "Smooth scroll animation");
2161 mCallee = aCallee;
2162 auto* presShell = mCallee->PresShell();
2163 MOZ_ASSERT(presShell);
2164 presShell->SuppressDisplayport(true);
2167 virtual void WillRefresh(mozilla::TimeStamp aTime) override {
2168 // The callback may release "this".
2169 // We don't access members after returning, so no need for KungFuDeathGrip.
2170 nsHTMLScrollFrame::AsyncScrollCallback(mCallee, aTime);
2174 * The mCallee holds a strong ref to us since the refresh driver doesn't.
2175 * Our dtor and mCallee's Destroy() method both call RemoveObserver() -
2176 * whichever comes first removes us from the refresh driver.
2178 void RemoveObserver() {
2179 if (mCallee) {
2180 RefreshDriver(mCallee)->RemoveRefreshObserver(this, FlushType::Style);
2181 auto* presShell = mCallee->PresShell();
2182 MOZ_ASSERT(presShell);
2183 presShell->SuppressDisplayport(false);
2184 mCallee = nullptr;
2188 UniquePtr<ScrollSnapTargetIds> TakeSnapTargetIds() {
2189 return std::move(mSnapTargetIds);
2192 bool WasTriggeredByScript() const {
2193 return mTriggeredByScript == ScrollTriggeredByScript::Yes;
2196 private:
2197 nsHTMLScrollFrame* mCallee;
2198 UniquePtr<ScrollSnapTargetIds> mSnapTargetIds;
2199 ScrollTriggeredByScript mTriggeredByScript;
2201 nsRefreshDriver* RefreshDriver(nsHTMLScrollFrame* aCallee) {
2202 return aCallee->PresContext()->RefreshDriver();
2206 void nsHTMLScrollFrame::AsyncScroll::InitSmoothScroll(
2207 TimeStamp aTime, nsPoint aInitialPosition, nsPoint aDestination,
2208 ScrollOrigin aOrigin, const nsRect& aRange,
2209 const nsSize& aCurrentVelocity) {
2210 switch (aOrigin) {
2211 case ScrollOrigin::NotSpecified:
2212 case ScrollOrigin::Restore:
2213 case ScrollOrigin::Relative:
2214 // We don't have special prefs for "restore", just treat it as "other".
2215 // "restore" scrolls are (for now) always instant anyway so unless
2216 // something changes we should never have aOrigin ==
2217 // ScrollOrigin::Restore here.
2218 aOrigin = ScrollOrigin::Other;
2219 break;
2220 case ScrollOrigin::Apz:
2221 // Likewise we should never get APZ-triggered scrolls here, and if that
2222 // changes something is likely broken somewhere.
2223 MOZ_ASSERT_UNREACHABLE(
2224 "APZ scroll position updates should never be smooth");
2225 break;
2226 case ScrollOrigin::AnchorAdjustment:
2227 MOZ_ASSERT_UNREACHABLE(
2228 "scroll anchor adjustments should never be smooth");
2229 break;
2230 default:
2231 break;
2234 // Read preferences only on first iteration or for a different event origin.
2235 if (!mAnimationPhysics || aOrigin != mOrigin) {
2236 mOrigin = aOrigin;
2237 if (StaticPrefs::general_smoothScroll_msdPhysics_enabled()) {
2238 mAnimationPhysics =
2239 MakeUnique<ScrollAnimationMSDPhysics>(aInitialPosition);
2240 } else {
2241 ScrollAnimationBezierPhysicsSettings settings =
2242 layers::apz::ComputeBezierAnimationSettingsForOrigin(mOrigin);
2243 mAnimationPhysics =
2244 MakeUnique<ScrollAnimationBezierPhysics>(aInitialPosition, settings);
2248 mStartPosition = aInitialPosition;
2249 mRange = aRange;
2251 mAnimationPhysics->Update(aTime, aDestination, aCurrentVelocity);
2255 * Callback function from AsyncSmoothMSDScroll, used in
2256 * nsHTMLScrollFrame::ScrollTo
2258 void nsHTMLScrollFrame::AsyncSmoothMSDScrollCallback(
2259 nsHTMLScrollFrame* aInstance, mozilla::TimeDuration aDeltaTime) {
2260 NS_ASSERTION(aInstance != nullptr, "aInstance must not be null");
2261 NS_ASSERTION(aInstance->mAsyncSmoothMSDScroll,
2262 "Did not expect AsyncSmoothMSDScrollCallback without an active "
2263 "MSD scroll.");
2265 nsRect range = aInstance->mAsyncSmoothMSDScroll->GetRange();
2266 aInstance->mAsyncSmoothMSDScroll->Simulate(aDeltaTime);
2268 if (!aInstance->mAsyncSmoothMSDScroll->IsFinished()) {
2269 nsPoint destination = aInstance->mAsyncSmoothMSDScroll->GetPosition();
2270 // Allow this scroll operation to land on any pixel boundary within the
2271 // allowed scroll range for this frame.
2272 // If the MSD is under-dampened or the destination is changed rapidly,
2273 // it is expected (and desired) that the scrolling may overshoot.
2274 nsRect intermediateRange = nsRect(destination, nsSize()).UnionEdges(range);
2275 aInstance->ScrollToImpl(destination, intermediateRange);
2276 // 'aInstance' might be destroyed here
2277 return;
2280 aInstance->CompleteAsyncScroll(
2281 aInstance->mAsyncSmoothMSDScroll->GetStartPosition(), range,
2282 aInstance->mAsyncSmoothMSDScroll->TakeSnapTargetIds());
2286 * Callback function from AsyncScroll, used in nsHTMLScrollFrame::ScrollTo
2288 void nsHTMLScrollFrame::AsyncScrollCallback(nsHTMLScrollFrame* aInstance,
2289 mozilla::TimeStamp aTime) {
2290 MOZ_ASSERT(aInstance != nullptr, "aInstance must not be null");
2291 MOZ_ASSERT(
2292 aInstance->mAsyncScroll,
2293 "Did not expect AsyncScrollCallback without an active async scroll.");
2295 if (!aInstance || !aInstance->mAsyncScroll) {
2296 return; // XXX wallpaper bug 1107353 for now.
2299 nsRect range = aInstance->mAsyncScroll->mRange;
2300 if (aInstance->mAsyncScroll->IsSmoothScroll()) {
2301 if (!aInstance->mAsyncScroll->IsFinished(aTime)) {
2302 nsPoint destination = aInstance->mAsyncScroll->PositionAt(aTime);
2303 // Allow this scroll operation to land on any pixel boundary between the
2304 // current position and the final allowed range. (We don't want
2305 // intermediate steps to be more constrained than the final step!)
2306 nsRect intermediateRange =
2307 nsRect(aInstance->GetScrollPosition(), nsSize()).UnionEdges(range);
2308 aInstance->ScrollToImpl(destination, intermediateRange);
2309 // 'aInstance' might be destroyed here
2310 return;
2314 aInstance->CompleteAsyncScroll(aInstance->mAsyncScroll->GetStartPosition(),
2315 range,
2316 aInstance->mAsyncScroll->TakeSnapTargetIds());
2319 void nsHTMLScrollFrame::SetTransformingByAPZ(bool aTransforming) {
2320 if (mTransformingByAPZ && !aTransforming) {
2321 PostScrollEndEvent();
2323 mTransformingByAPZ = aTransforming;
2324 if (!mozilla::css::TextOverflow::HasClippedTextOverflow(this) ||
2325 mozilla::css::TextOverflow::HasBlockEllipsis(mScrolledFrame)) {
2326 // If the block has some overflow marker stuff we should kick off a paint
2327 // because we have special behaviour for it when APZ scrolling is active.
2328 SchedulePaint();
2332 void nsHTMLScrollFrame::CompleteAsyncScroll(
2333 const nsPoint& aStartPosition, const nsRect& aRange,
2334 UniquePtr<ScrollSnapTargetIds> aSnapTargetIds, ScrollOrigin aOrigin) {
2335 SetLastSnapTargetIds(std::move(aSnapTargetIds));
2337 bool scrollPositionChanged = mDestination != aStartPosition;
2338 bool isNotHandledByApz =
2339 nsLayoutUtils::CanScrollOriginClobberApz(aOrigin) ||
2340 ScrollAnimationState().contains(AnimationState::MainThread);
2342 // Apply desired destination range since this is the last step of scrolling.
2343 RemoveObservers();
2344 AutoWeakFrame weakFrame(this);
2345 ScrollToImpl(mDestination, aRange, aOrigin);
2346 if (!weakFrame.IsAlive()) {
2347 return;
2349 // We are done scrolling, set our destination to wherever we actually ended
2350 // up scrolling to.
2351 mDestination = GetScrollPosition();
2352 // Post a `scrollend` event for scrolling not handled by APZ, including:
2354 // - programmatic instant scrolls
2355 // - the end of a smooth scroll animation running on the main thread
2357 // For scrolling handled by APZ, the `scrollend` event is posted in
2358 // SetTransformingByAPZ() when the APZC is transitioning from a transforming
2359 // to a non-transforming state (e.g. a transition from PANNING to NOTHING).
2360 // The scrollend event should not be fired for a scroll that does not
2361 // result in a scroll position change.
2362 if (isNotHandledByApz && scrollPositionChanged) {
2363 PostScrollEndEvent();
2367 bool nsHTMLScrollFrame::HasBgAttachmentLocal() const {
2368 const nsStyleBackground* bg = StyleBackground();
2369 return bg->HasLocalBackground();
2372 void nsHTMLScrollFrame::ScrollToInternal(
2373 nsPoint aScrollPosition, ScrollMode aMode, ScrollOrigin aOrigin,
2374 const nsRect* aRange, ScrollSnapFlags aSnapFlags,
2375 ScrollTriggeredByScript aTriggeredByScript) {
2376 if (aOrigin == ScrollOrigin::NotSpecified) {
2377 aOrigin = ScrollOrigin::Other;
2379 ScrollToWithOrigin(
2380 aScrollPosition, aRange,
2381 ScrollOperationParams{aMode, aOrigin, aSnapFlags, aTriggeredByScript});
2384 void nsHTMLScrollFrame::ScrollToCSSPixels(const CSSIntPoint& aScrollPosition,
2385 ScrollMode aMode) {
2386 CSSIntPoint currentCSSPixels = GetScrollPositionCSSPixels();
2387 // Transmogrify this scroll to a relative one if there's any on-going
2388 // animation in APZ triggered by __user__.
2389 // Bug 1740164: We will apply it for cases there's no animation in APZ.
2391 auto scrollAnimationState = ScrollAnimationState();
2392 bool isScrollAnimating =
2393 scrollAnimationState.contains(AnimationState::MainThread) ||
2394 scrollAnimationState.contains(AnimationState::APZPending) ||
2395 scrollAnimationState.contains(AnimationState::APZRequested);
2396 if (mCurrentAPZScrollAnimationType ==
2397 APZScrollAnimationType::TriggeredByUserInput &&
2398 !isScrollAnimating) {
2399 CSSIntPoint delta = aScrollPosition - currentCSSPixels;
2400 // This transmogrification need to be an intended end position scroll
2401 // operation.
2402 ScrollByCSSPixelsInternal(delta, aMode,
2403 ScrollSnapFlags::IntendedEndPosition);
2404 return;
2407 nscoord halfPixel = nsPresContext::CSSPixelsToAppUnits(0.5f);
2408 nsPoint pt = CSSPoint::ToAppUnits(aScrollPosition);
2409 nsRect range(pt.x - halfPixel, pt.y - halfPixel, 2 * halfPixel - 1,
2410 2 * halfPixel - 1);
2411 // XXX I don't think the following blocks are needed anymore, now that
2412 // ScrollToImpl simply tries to scroll an integer number of layer
2413 // pixels from the current position
2414 nsPoint current = GetScrollPosition();
2415 if (currentCSSPixels.x == aScrollPosition.x) {
2416 pt.x = current.x;
2417 range.x = pt.x;
2418 range.width = 0;
2420 if (currentCSSPixels.y == aScrollPosition.y) {
2421 pt.y = current.y;
2422 range.y = pt.y;
2423 range.height = 0;
2425 ScrollToWithOrigin(
2426 pt, &range,
2427 ScrollOperationParams{
2428 aMode, ScrollOrigin::Other,
2429 // This ScrollToCSSPixels is used for Element.scrollTo,
2430 // Element.scrollTop, Element.scrollLeft and for Window.scrollTo.
2431 ScrollSnapFlags::IntendedEndPosition, ScrollTriggeredByScript::Yes});
2432 // 'this' might be destroyed here
2435 void nsHTMLScrollFrame::ScrollToCSSPixelsForApz(
2436 const CSSPoint& aScrollPosition, ScrollSnapTargetIds&& aLastSnapTargetIds) {
2437 nsPoint pt = CSSPoint::ToAppUnits(aScrollPosition);
2438 nscoord halfRange = nsPresContext::CSSPixelsToAppUnits(1000);
2439 nsRect range(pt.x - halfRange, pt.y - halfRange, 2 * halfRange - 1,
2440 2 * halfRange - 1);
2441 ScrollToWithOrigin(
2442 pt, &range,
2443 ScrollOperationParams{ScrollMode::Instant, ScrollOrigin::Apz,
2444 std::move(aLastSnapTargetIds)});
2445 // 'this' might be destroyed here
2448 CSSIntPoint nsHTMLScrollFrame::GetScrollPositionCSSPixels() {
2449 return CSSIntPoint::FromAppUnitsRounded(GetScrollPosition());
2453 * this method wraps calls to ScrollToImpl(), either in one shot or
2454 * incrementally, based on the setting of the smoothness scroll pref
2456 void nsHTMLScrollFrame::ScrollToWithOrigin(nsPoint aScrollPosition,
2457 const nsRect* aRange,
2458 ScrollOperationParams&& aParams) {
2459 // None is never a valid scroll origin to be passed in.
2460 MOZ_ASSERT(aParams.mOrigin != ScrollOrigin::None);
2462 if (aParams.mOrigin != ScrollOrigin::Restore) {
2463 // If we're doing a non-restore scroll, we don't want to later
2464 // override it by restoring our saved scroll position.
2465 SCROLLRESTORE_LOG("%p: Clearing mRestorePos (cur=%s, dst=%s)\n", this,
2466 ToString(GetScrollPosition()).c_str(),
2467 ToString(aScrollPosition).c_str());
2468 mRestorePos.x = mRestorePos.y = -1;
2471 Maybe<SnapDestination> snapDestination;
2472 if (!aParams.IsScrollSnapDisabled()) {
2473 snapDestination = GetSnapPointForDestination(ScrollUnit::DEVICE_PIXELS,
2474 aParams.mSnapFlags,
2475 mDestination, aScrollPosition);
2476 if (snapDestination) {
2477 aScrollPosition = snapDestination->mPosition;
2481 nsRect scrollRange = GetLayoutScrollRange();
2482 mDestination = scrollRange.ClampPoint(aScrollPosition);
2483 if (mDestination != aScrollPosition &&
2484 aParams.mOrigin == ScrollOrigin::Restore &&
2485 GetPageLoadingState() != LoadingState::Loading) {
2486 // If we're doing a restore but the scroll position is clamped, promote
2487 // the origin from one that APZ can clobber to one that it can't clobber.
2488 aParams.mOrigin = ScrollOrigin::Other;
2491 nsRect range = aRange && snapDestination.isNothing()
2492 ? *aRange
2493 : nsRect(aScrollPosition, nsSize(0, 0));
2495 UniquePtr<ScrollSnapTargetIds> snapTargetIds;
2496 if (snapDestination) {
2497 snapTargetIds =
2498 MakeUnique<ScrollSnapTargetIds>(std::move(snapDestination->mTargetIds));
2499 } else {
2500 snapTargetIds =
2501 MakeUnique<ScrollSnapTargetIds>(std::move(aParams.mTargetIds));
2503 if (aParams.IsInstant()) {
2504 // Asynchronous scrolling is not allowed, so we'll kill any existing
2505 // async-scrolling process and do an instant scroll.
2506 CompleteAsyncScroll(GetScrollPosition(), range, std::move(snapTargetIds),
2507 aParams.mOrigin);
2508 mApzSmoothScrollDestination = Nothing();
2509 return;
2512 if (!aParams.IsSmoothMsd()) {
2513 // If we get a non-smooth-scroll, reset the cached APZ scroll destination,
2514 // so that we know to process the next smooth-scroll destined for APZ.
2515 mApzSmoothScrollDestination = Nothing();
2518 nsPresContext* presContext = PresContext();
2519 TimeStamp now =
2520 presContext->RefreshDriver()->IsTestControllingRefreshesEnabled()
2521 ? presContext->RefreshDriver()->MostRecentRefresh()
2522 : TimeStamp::Now();
2524 nsSize currentVelocity(0, 0);
2526 const bool canHandoffToApz =
2527 nsLayoutUtils::AsyncPanZoomEnabled(this) && WantAsyncScroll() &&
2528 CanApzScrollInTheseDirections(
2529 DirectionsInDelta(mDestination - GetScrollPosition()));
2531 if (aParams.IsSmoothMsd()) {
2532 mIgnoreMomentumScroll = true;
2533 if (!mAsyncSmoothMSDScroll) {
2534 nsPoint sv = mVelocityQueue.GetVelocity();
2535 currentVelocity.width = sv.x;
2536 currentVelocity.height = sv.y;
2537 if (mAsyncScroll) {
2538 if (mAsyncScroll->IsSmoothScroll()) {
2539 currentVelocity = mAsyncScroll->VelocityAt(now);
2541 mAsyncScroll = nullptr;
2544 if (canHandoffToApz) {
2545 ApzSmoothScrollTo(mDestination, ScrollMode::SmoothMsd, aParams.mOrigin,
2546 aParams.mTriggeredByScript, std::move(snapTargetIds));
2547 return;
2550 mAsyncSmoothMSDScroll = new AsyncSmoothMSDScroll(
2551 GetScrollPosition(), mDestination, currentVelocity,
2552 GetLayoutScrollRange(), now, presContext, std::move(snapTargetIds),
2553 aParams.mTriggeredByScript);
2555 mAsyncSmoothMSDScroll->SetRefreshObserver(this);
2556 } else {
2557 // A previous smooth MSD scroll is still in progress, so we just need to
2558 // update its range and destination.
2559 mAsyncSmoothMSDScroll->SetRange(GetLayoutScrollRange());
2560 mAsyncSmoothMSDScroll->SetDestination(mDestination,
2561 aParams.mTriggeredByScript);
2564 return;
2567 if (mAsyncSmoothMSDScroll) {
2568 currentVelocity = mAsyncSmoothMSDScroll->GetVelocity();
2569 mAsyncSmoothMSDScroll = nullptr;
2572 const bool isSmoothScroll =
2573 aParams.IsSmooth() && nsLayoutUtils::IsSmoothScrollingEnabled();
2574 if (!mAsyncScroll) {
2575 if (isSmoothScroll && canHandoffToApz) {
2576 ApzSmoothScrollTo(mDestination, ScrollMode::Smooth, aParams.mOrigin,
2577 aParams.mTriggeredByScript, std::move(snapTargetIds));
2578 return;
2581 mAsyncScroll =
2582 new AsyncScroll(std::move(snapTargetIds), aParams.mTriggeredByScript);
2583 mAsyncScroll->SetRefreshObserver(this);
2586 if (isSmoothScroll) {
2587 mAsyncScroll->InitSmoothScroll(now, GetScrollPosition(), mDestination,
2588 aParams.mOrigin, range, currentVelocity);
2589 } else {
2590 mAsyncScroll->Init(GetScrollPosition(), range);
2594 // We can't use nsContainerFrame::PositionChildViews here because
2595 // we don't want to invalidate views that have moved.
2596 static void AdjustViews(nsIFrame* aFrame) {
2597 nsView* view = aFrame->GetView();
2598 if (view) {
2599 nsPoint pt;
2600 aFrame->GetParent()->GetClosestView(&pt);
2601 pt += aFrame->GetPosition();
2602 view->SetPosition(pt.x, pt.y);
2604 return;
2607 if (!aFrame->HasAnyStateBits(NS_FRAME_HAS_CHILD_WITH_VIEW)) {
2608 return;
2611 // Call AdjustViews recursively for all child frames except the popup list as
2612 // the views for popups are not scrolled.
2613 for (const auto& [list, listID] : aFrame->ChildLists()) {
2614 if (listID == FrameChildListID::Popup) {
2615 continue;
2617 for (nsIFrame* child : list) {
2618 AdjustViews(child);
2623 void nsHTMLScrollFrame::MarkScrollbarsDirtyForReflow() const {
2624 auto* presShell = PresShell();
2625 if (mVScrollbarBox) {
2626 presShell->FrameNeedsReflow(mVScrollbarBox,
2627 IntrinsicDirty::FrameAncestorsAndDescendants,
2628 NS_FRAME_IS_DIRTY);
2630 if (mHScrollbarBox) {
2631 presShell->FrameNeedsReflow(mHScrollbarBox,
2632 IntrinsicDirty::FrameAncestorsAndDescendants,
2633 NS_FRAME_IS_DIRTY);
2637 void nsHTMLScrollFrame::InvalidateScrollbars() const {
2638 if (mHScrollbarBox) {
2639 mHScrollbarBox->InvalidateFrameSubtree();
2641 if (mVScrollbarBox) {
2642 mVScrollbarBox->InvalidateFrameSubtree();
2646 bool nsHTMLScrollFrame::IsAlwaysActive() const {
2647 if (nsDisplayItem::ForceActiveLayers()) {
2648 return true;
2651 // Unless this is the root scrollframe for a non-chrome document
2652 // which is the direct child of a chrome document, we default to not
2653 // being "active".
2654 if (!(mIsRoot && PresContext()->IsRootContentDocumentCrossProcess())) {
2655 return false;
2658 // If we have scrolled before, then we should stay active.
2659 if (mHasBeenScrolled) {
2660 return true;
2663 // If we're overflow:hidden, then start as inactive until
2664 // we get scrolled manually.
2665 ScrollStyles styles = GetScrollStyles();
2666 return (styles.mHorizontal != StyleOverflow::Hidden &&
2667 styles.mVertical != StyleOverflow::Hidden);
2670 static void RemoveDisplayPortCallback(nsITimer* aTimer, void* aClosure) {
2671 nsHTMLScrollFrame* sf = static_cast<nsHTMLScrollFrame*>(aClosure);
2673 // This function only ever gets called from the expiry timer, so it must
2674 // be non-null here. Set it to null here so that we don't keep resetting
2675 // it unnecessarily in MarkRecentlyScrolled().
2676 MOZ_ASSERT(sf->mDisplayPortExpiryTimer);
2677 sf->mDisplayPortExpiryTimer = nullptr;
2679 if (!sf->AllowDisplayPortExpiration() || sf->mIsParentToActiveScrollFrames) {
2680 // If this is a scroll parent for some other scrollable frame, don't
2681 // expire the displayport because it would break scroll handoff. Once the
2682 // descendant scrollframes have their displayports expired, they will
2683 // trigger the displayport expiration on this scrollframe as well, and
2684 // mIsParentToActiveScrollFrames will presumably be false when that kicks
2685 // in.
2686 return;
2689 // Remove the displayport from this scrollframe if it's been a while
2690 // since it's scrolled, except if it needs to be always active. Note that
2691 // there is one scrollframe that doesn't fall under this general rule, and
2692 // that is the one that nsLayoutUtils::MaybeCreateDisplayPort decides to put
2693 // a displayport on (i.e. the first scrollframe that WantAsyncScroll()s).
2694 // If that scrollframe is this one, we remove the displayport anyway, and
2695 // as part of the next paint MaybeCreateDisplayPort will put another
2696 // displayport back on it. Although the displayport will "flicker" off and
2697 // back on, the layer itself should never disappear, because this all
2698 // happens between actual painting. If the displayport is reset to a
2699 // different position that's ok; this scrollframe hasn't been scrolled
2700 // recently and so the reset should be correct.
2702 nsIContent* content = sf->GetContent();
2704 if (nsHTMLScrollFrame::ShouldActivateAllScrollFrames()) {
2705 // If we are activating all scroll frames then we only want to remove the
2706 // regular display port and downgrade to a minimal display port.
2707 MOZ_ASSERT(!content->GetProperty(nsGkAtoms::MinimalDisplayPort));
2708 content->SetProperty(nsGkAtoms::MinimalDisplayPort,
2709 reinterpret_cast<void*>(true));
2710 } else {
2711 content->RemoveProperty(nsGkAtoms::MinimalDisplayPort);
2712 DisplayPortUtils::RemoveDisplayPort(content);
2713 // Be conservative and unflag this this scrollframe as being scrollable by
2714 // APZ. If it is still scrollable this will get flipped back soon enough.
2715 sf->mScrollableByAPZ = false;
2718 DisplayPortUtils::ExpireDisplayPortOnAsyncScrollableAncestor(sf);
2719 sf->SchedulePaint();
2722 void nsHTMLScrollFrame::MarkEverScrolled() {
2723 // Mark this frame as having been scrolled. If this is the root
2724 // scroll frame of a content document, then IsAlwaysActive()
2725 // will return true from now on and MarkNotRecentlyScrolled() won't
2726 // have any effect.
2727 mHasBeenScrolled = true;
2730 void nsHTMLScrollFrame::MarkNotRecentlyScrolled() {
2731 if (!mHasBeenScrolledRecently) return;
2733 mHasBeenScrolledRecently = false;
2734 SchedulePaint();
2737 void nsHTMLScrollFrame::MarkRecentlyScrolled() {
2738 mHasBeenScrolledRecently = true;
2739 if (IsAlwaysActive()) {
2740 return;
2743 if (mActivityExpirationState.IsTracked()) {
2744 gScrollFrameActivityTracker->MarkUsed(this);
2745 } else {
2746 if (!gScrollFrameActivityTracker) {
2747 gScrollFrameActivityTracker =
2748 new ScrollFrameActivityTracker(GetMainThreadSerialEventTarget());
2750 gScrollFrameActivityTracker->AddObject(this);
2753 // If we just scrolled and there's a displayport expiry timer in place,
2754 // reset the timer.
2755 ResetDisplayPortExpiryTimer();
2758 void nsHTMLScrollFrame::ResetDisplayPortExpiryTimer() {
2759 if (mDisplayPortExpiryTimer) {
2760 mDisplayPortExpiryTimer->InitWithNamedFuncCallback(
2761 RemoveDisplayPortCallback, this,
2762 StaticPrefs::apz_displayport_expiry_ms(), nsITimer::TYPE_ONE_SHOT,
2763 "nsHTMLScrollFrame::ResetDisplayPortExpiryTimer");
2767 bool nsHTMLScrollFrame::AllowDisplayPortExpiration() {
2768 if (IsAlwaysActive()) {
2769 return false;
2772 if (mIsRoot && PresContext()->IsRoot()) {
2773 return false;
2776 // If this was the first scrollable frame found, this displayport should
2777 // not expire.
2778 if (IsFirstScrollableFrameSequenceNumber().isSome()) {
2779 return false;
2782 if (ShouldActivateAllScrollFrames() &&
2783 GetContent()->GetProperty(nsGkAtoms::MinimalDisplayPort)) {
2784 return false;
2786 return true;
2789 void nsHTMLScrollFrame::TriggerDisplayPortExpiration() {
2790 if (!AllowDisplayPortExpiration()) {
2791 return;
2794 if (!StaticPrefs::apz_displayport_expiry_ms()) {
2795 // a zero time disables the expiry
2796 return;
2799 if (!mDisplayPortExpiryTimer) {
2800 mDisplayPortExpiryTimer = NS_NewTimer();
2802 ResetDisplayPortExpiryTimer();
2805 void nsHTMLScrollFrame::ScrollVisual() {
2806 MarkEverScrolled();
2808 AdjustViews(mScrolledFrame);
2809 // We need to call this after fixing up the view positions
2810 // to be consistent with the frame hierarchy.
2811 MarkRecentlyScrolled();
2815 * Clamp desired scroll position aDesired and range [aDestLower, aDestUpper]
2816 * to [aBoundLower, aBoundUpper] and then select the appunit value from among
2817 * aBoundLower, aBoundUpper and those such that (aDesired - aCurrent) *
2818 * aRes/aAppUnitsPerPixel is an integer (or as close as we can get
2819 * modulo rounding to appunits) that is in [aDestLower, aDestUpper] and
2820 * closest to aDesired. If no such value exists, return the nearest in
2821 * [aDestLower, aDestUpper].
2823 static nscoord ClampAndAlignWithPixels(nscoord aDesired, nscoord aBoundLower,
2824 nscoord aBoundUpper, nscoord aDestLower,
2825 nscoord aDestUpper,
2826 nscoord aAppUnitsPerPixel, double aRes,
2827 nscoord aCurrent) {
2828 // Intersect scroll range with allowed range, by clamping the ends
2829 // of aRange to be within bounds
2830 nscoord destLower = clamped(aDestLower, aBoundLower, aBoundUpper);
2831 nscoord destUpper = clamped(aDestUpper, aBoundLower, aBoundUpper);
2833 nscoord desired = clamped(aDesired, destLower, destUpper);
2834 if (StaticPrefs::layout_scroll_disable_pixel_alignment()) {
2835 return desired;
2838 double currentLayerVal = (aRes * aCurrent) / aAppUnitsPerPixel;
2839 double desiredLayerVal = (aRes * desired) / aAppUnitsPerPixel;
2840 double delta = desiredLayerVal - currentLayerVal;
2841 double nearestLayerVal = NS_round(delta) + currentLayerVal;
2843 // Convert back from PaintedLayer space to appunits relative to the top-left
2844 // of the scrolled frame.
2845 nscoord aligned =
2846 aRes == 0.0
2847 ? 0.0
2848 : NSToCoordRoundWithClamp(nearestLayerVal * aAppUnitsPerPixel / aRes);
2850 // Use a bound if it is within the allowed range and closer to desired than
2851 // the nearest pixel-aligned value.
2852 if (aBoundUpper == destUpper &&
2853 static_cast<decltype(Abs(desired))>(aBoundUpper - desired) <
2854 Abs(desired - aligned)) {
2855 return aBoundUpper;
2858 if (aBoundLower == destLower &&
2859 static_cast<decltype(Abs(desired))>(desired - aBoundLower) <
2860 Abs(aligned - desired)) {
2861 return aBoundLower;
2864 // Accept the nearest pixel-aligned value if it is within the allowed range.
2865 if (aligned >= destLower && aligned <= destUpper) {
2866 return aligned;
2869 // Check if opposite pixel boundary fits into allowed range.
2870 double oppositeLayerVal =
2871 nearestLayerVal + ((nearestLayerVal < desiredLayerVal) ? 1.0 : -1.0);
2872 nscoord opposite = aRes == 0.0
2873 ? 0.0
2874 : NSToCoordRoundWithClamp(oppositeLayerVal *
2875 aAppUnitsPerPixel / aRes);
2876 if (opposite >= destLower && opposite <= destUpper) {
2877 return opposite;
2880 // No alignment available.
2881 return desired;
2885 * Clamp desired scroll position aPt to aBounds and then snap
2886 * it to the same layer pixel edges as aCurrent, keeping it within aRange
2887 * during snapping. aCurrent is the current scroll position.
2889 static nsPoint ClampAndAlignWithLayerPixels(const nsPoint& aPt,
2890 const nsRect& aBounds,
2891 const nsRect& aRange,
2892 const nsPoint& aCurrent,
2893 nscoord aAppUnitsPerPixel,
2894 const MatrixScales& aScale) {
2895 return nsPoint(
2896 ClampAndAlignWithPixels(aPt.x, aBounds.x, aBounds.XMost(), aRange.x,
2897 aRange.XMost(), aAppUnitsPerPixel, aScale.xScale,
2898 aCurrent.x),
2899 ClampAndAlignWithPixels(aPt.y, aBounds.y, aBounds.YMost(), aRange.y,
2900 aRange.YMost(), aAppUnitsPerPixel, aScale.yScale,
2901 aCurrent.y));
2904 /* static */
2905 void nsHTMLScrollFrame::ScrollActivityCallback(nsITimer* aTimer,
2906 void* anInstance) {
2907 nsHTMLScrollFrame* self = static_cast<nsHTMLScrollFrame*>(anInstance);
2909 // Fire the synth mouse move.
2910 self->mScrollActivityTimer->Cancel();
2911 self->mScrollActivityTimer = nullptr;
2912 self->PresShell()->SynthesizeMouseMove(true);
2915 void nsHTMLScrollFrame::ScheduleSyntheticMouseMove() {
2916 if (!mScrollActivityTimer) {
2917 mScrollActivityTimer = NS_NewTimer(GetMainThreadSerialEventTarget());
2918 if (!mScrollActivityTimer) {
2919 return;
2923 mScrollActivityTimer->InitWithNamedFuncCallback(
2924 ScrollActivityCallback, this, 100, nsITimer::TYPE_ONE_SHOT,
2925 "nsHTMLScrollFrame::ScheduleSyntheticMouseMove");
2928 void nsHTMLScrollFrame::NotifyApproximateFrameVisibilityUpdate(
2929 bool aIgnoreDisplayPort) {
2930 mLastUpdateFramesPos = GetScrollPosition();
2931 if (aIgnoreDisplayPort) {
2932 mHadDisplayPortAtLastFrameUpdate = false;
2933 mDisplayPortAtLastFrameUpdate = nsRect();
2934 } else {
2935 mHadDisplayPortAtLastFrameUpdate = DisplayPortUtils::GetDisplayPort(
2936 GetContent(), &mDisplayPortAtLastFrameUpdate);
2940 bool nsHTMLScrollFrame::GetDisplayPortAtLastApproximateFrameVisibilityUpdate(
2941 nsRect* aDisplayPort) {
2942 if (mHadDisplayPortAtLastFrameUpdate) {
2943 *aDisplayPort = mDisplayPortAtLastFrameUpdate;
2945 return mHadDisplayPortAtLastFrameUpdate;
2948 /* aIncludeCSSTransform controls if we include CSS transforms that are in this
2949 * process (the BrowserChild EffectsInfo mTransformToAncestorScale will include
2950 * CSS transforms in ancestor processes in all cases). */
2951 MatrixScales GetPaintedLayerScaleForFrame(nsIFrame* aFrame,
2952 bool aIncludeCSSTransform) {
2953 MOZ_ASSERT(aFrame, "need a frame");
2955 nsPresContext* presCtx = aFrame->PresContext()->GetRootPresContext();
2957 if (!presCtx) {
2958 presCtx = aFrame->PresContext();
2959 MOZ_ASSERT(presCtx);
2962 ParentLayerToScreenScale2D transformToAncestorScale;
2963 if (aIncludeCSSTransform) {
2964 transformToAncestorScale =
2965 nsLayoutUtils::GetTransformToAncestorScaleCrossProcessForFrameMetrics(
2966 aFrame);
2967 } else {
2968 if (BrowserChild* browserChild =
2969 BrowserChild::GetFrom(aFrame->PresShell())) {
2970 transformToAncestorScale =
2971 browserChild->GetEffectsInfo().mTransformToAncestorScale;
2974 transformToAncestorScale =
2975 ParentLayerToParentLayerScale(
2976 presCtx->PresShell()->GetCumulativeResolution()) *
2977 transformToAncestorScale;
2979 return transformToAncestorScale.ToUnknownScale();
2982 void nsHTMLScrollFrame::ScrollToImpl(
2983 nsPoint aPt, const nsRect& aRange, ScrollOrigin aOrigin,
2984 ScrollTriggeredByScript aTriggeredByScript) {
2985 // None is never a valid scroll origin to be passed in.
2986 MOZ_ASSERT(aOrigin != ScrollOrigin::None);
2988 // Figure out the effective origin for this scroll request.
2989 if (aOrigin == ScrollOrigin::NotSpecified) {
2990 // If no origin was specified, we still want to set it to something that's
2991 // non-unknown, so that we can use eUnknown to distinguish if the frame was
2992 // scrolled at all. Default it to some generic placeholder.
2993 aOrigin = ScrollOrigin::Other;
2996 // If this scroll is |relative|, but we've already had a user scroll that
2997 // was not relative, promote this origin to |other|. This ensures that we
2998 // may only transmit a relative update to APZ if all scrolls since the last
2999 // transaction or repaint request have been relative.
3000 if (aOrigin == ScrollOrigin::Relative &&
3001 (mLastScrollOrigin != ScrollOrigin::None &&
3002 mLastScrollOrigin != ScrollOrigin::NotSpecified &&
3003 mLastScrollOrigin != ScrollOrigin::Relative &&
3004 mLastScrollOrigin != ScrollOrigin::AnchorAdjustment &&
3005 mLastScrollOrigin != ScrollOrigin::Apz)) {
3006 aOrigin = ScrollOrigin::Other;
3009 // If the origin is a downgrade, and downgrades are allowed, process the
3010 // downgrade even if we're going to early-exit because we're already at
3011 // the correct scroll position. This ensures that if there wasn't a main-
3012 // thread scroll update pending before a frame reconstruction (as indicated
3013 // by mAllowScrollOriginDowngrade=true), then after the frame reconstruction
3014 // the origin is downgraded to "restore" even if the layout scroll offset to
3015 // be restored is (0,0) (which will take the early-exit below). This is
3016 // important so that restoration of a *visual* scroll offset (which might be
3017 // to something other than (0,0)) isn't clobbered.
3018 bool isScrollOriginDowngrade =
3019 nsLayoutUtils::CanScrollOriginClobberApz(mLastScrollOrigin) &&
3020 !nsLayoutUtils::CanScrollOriginClobberApz(aOrigin);
3021 bool allowScrollOriginChange =
3022 mAllowScrollOriginDowngrade && isScrollOriginDowngrade;
3024 if (allowScrollOriginChange) {
3025 mLastScrollOrigin = aOrigin;
3026 mAllowScrollOriginDowngrade = false;
3029 nsPresContext* presContext = PresContext();
3030 nscoord appUnitsPerDevPixel = presContext->AppUnitsPerDevPixel();
3031 // 'scale' is our estimate of the scale factor that will be applied
3032 // when rendering the scrolled content to its own PaintedLayer.
3033 MatrixScales scale = GetPaintedLayerScaleForFrame(
3034 mScrolledFrame, /* aIncludeCSSTransform = */ true);
3035 nsPoint curPos = GetScrollPosition();
3037 // Try to align aPt with curPos so they have an integer number of layer
3038 // pixels between them. This gives us the best chance of scrolling without
3039 // having to invalidate due to changes in subpixel rendering.
3040 // Note that when we actually draw into a PaintedLayer, the coordinates
3041 // that get mapped onto the layer buffer pixels are from the display list,
3042 // which are relative to the display root frame's top-left increasing down,
3043 // whereas here our coordinates are scroll positions which increase upward
3044 // and are relative to the scrollport top-left. This difference doesn't
3045 // actually matter since all we are about is that there be an integer number
3046 // of layer pixels between pt and curPos.
3047 nsPoint pt = ClampAndAlignWithLayerPixels(aPt, GetLayoutScrollRange(), aRange,
3048 curPos, appUnitsPerDevPixel, scale);
3049 if (pt == curPos) {
3050 // Even if we are bailing out due to no-op main-thread scroll position
3051 // change, we might need to cancel an APZ smooth scroll that we already
3052 // kicked off. It might be reasonable to eventually remove the
3053 // mApzSmoothScrollDestination clause from this if statement, as that
3054 // may simplify this a bit and should be fine from the APZ side.
3055 if (mApzSmoothScrollDestination && aOrigin != ScrollOrigin::Clamp) {
3056 if (aOrigin == ScrollOrigin::Relative) {
3057 AppendScrollUpdate(ScrollPositionUpdate::NewRelativeScroll(
3058 // Clamp |mApzScrollPos| here. See the comment for this clamping
3059 // reason below NewRelativeScroll call.
3060 GetLayoutScrollRange().ClampPoint(mApzScrollPos), pt));
3061 mApzScrollPos = pt;
3062 } else if (aOrigin != ScrollOrigin::Apz) {
3063 ScrollOrigin origin =
3064 (mAllowScrollOriginDowngrade || !isScrollOriginDowngrade)
3065 ? aOrigin
3066 : mLastScrollOrigin;
3067 AppendScrollUpdate(ScrollPositionUpdate::NewScroll(origin, pt));
3070 return;
3073 // If we are scrolling the RCD-RSF, and a visual scroll update is pending,
3074 // cancel it; otherwise, it will clobber this scroll.
3075 if (IsRootScrollFrameOfDocument() &&
3076 presContext->IsRootContentDocumentCrossProcess()) {
3077 auto* ps = presContext->GetPresShell();
3078 if (const auto& visualScrollUpdate = ps->GetPendingVisualScrollUpdate()) {
3079 if (visualScrollUpdate->mVisualScrollOffset != aPt) {
3080 // Only clobber if the scroll was originated by the main thread.
3081 // Respect the priority of origins (an "eRestore" layout scroll should
3082 // not clobber an "eMainThread" visual scroll.)
3083 bool shouldClobber =
3084 aOrigin == ScrollOrigin::Other ||
3085 (aOrigin == ScrollOrigin::Restore &&
3086 visualScrollUpdate->mUpdateType == FrameMetrics::eRestore);
3087 if (shouldClobber) {
3088 ps->AcknowledgePendingVisualScrollUpdate();
3089 ps->ClearPendingVisualScrollUpdate();
3095 bool needFrameVisibilityUpdate = mLastUpdateFramesPos == nsPoint(-1, -1);
3097 nsPoint dist(std::abs(pt.x - mLastUpdateFramesPos.x),
3098 std::abs(pt.y - mLastUpdateFramesPos.y));
3099 nsSize visualViewportSize = GetVisualViewportSize();
3100 nscoord horzAllowance = std::max(
3101 visualViewportSize.width /
3102 std::max(
3103 StaticPrefs::
3104 layout_framevisibility_amountscrollbeforeupdatehorizontal(),
3106 AppUnitsPerCSSPixel());
3107 nscoord vertAllowance = std::max(
3108 visualViewportSize.height /
3109 std::max(
3110 StaticPrefs::
3111 layout_framevisibility_amountscrollbeforeupdatevertical(),
3113 AppUnitsPerCSSPixel());
3114 if (dist.x >= horzAllowance || dist.y >= vertAllowance) {
3115 needFrameVisibilityUpdate = true;
3118 // notify the listeners.
3119 for (uint32_t i = 0; i < mListeners.Length(); i++) {
3120 mListeners[i]->ScrollPositionWillChange(pt.x, pt.y);
3123 nsRect oldDisplayPort;
3124 nsIContent* content = GetContent();
3125 DisplayPortUtils::GetDisplayPort(content, &oldDisplayPort);
3126 oldDisplayPort.MoveBy(-mScrolledFrame->GetPosition());
3128 // Update frame position for scrolling
3129 mScrolledFrame->SetPosition(mScrollPort.TopLeft() - pt);
3131 // If |mLastScrollOrigin| is already set to something that can clobber APZ's
3132 // scroll offset, then we don't want to change it to something that can't.
3133 // If we allowed this, then we could end up in a state where APZ ignores
3134 // legitimate scroll offset updates because the origin has been masked by
3135 // a later change within the same refresh driver tick.
3136 allowScrollOriginChange =
3137 (mAllowScrollOriginDowngrade || !isScrollOriginDowngrade);
3139 if (allowScrollOriginChange) {
3140 mLastScrollOrigin = aOrigin;
3141 mAllowScrollOriginDowngrade = false;
3144 if (aOrigin == ScrollOrigin::Relative) {
3145 MOZ_ASSERT(!isScrollOriginDowngrade);
3146 MOZ_ASSERT(mLastScrollOrigin == ScrollOrigin::Relative);
3147 AppendScrollUpdate(ScrollPositionUpdate::NewRelativeScroll(
3148 // It's possible that |mApzScrollPos| is no longer within the scroll
3149 // range, we need to clamp it to the current scroll range, otherwise
3150 // calculating a relative scroll distance from the outside point will
3151 // result a point far from the desired point.
3152 GetLayoutScrollRange().ClampPoint(mApzScrollPos), pt));
3153 mApzScrollPos = pt;
3154 } else if (aOrigin == ScrollOrigin::AnchorAdjustment) {
3155 AppendScrollUpdate(ScrollPositionUpdate::NewMergeableScroll(aOrigin, pt));
3156 } else if (aOrigin != ScrollOrigin::Apz) {
3157 AppendScrollUpdate(ScrollPositionUpdate::NewScroll(mLastScrollOrigin, pt));
3160 if (mLastScrollOrigin == ScrollOrigin::Apz) {
3161 mApzScrollPos = GetScrollPosition();
3164 ScrollVisual();
3165 mAnchor.UserScrolled();
3167 // Only report user-triggered scrolling interactions
3168 bool jsOnStack = nsContentUtils::GetCurrentJSContext() != nullptr;
3169 bool scrollingToAnchor = ScrollingInteractionContext::IsScrollingToAnchor();
3170 if (!jsOnStack && !scrollingToAnchor) {
3171 nsPoint distanceScrolled(std::abs(pt.x - curPos.x),
3172 std::abs(pt.y - curPos.y));
3173 ScrollingMetrics::OnScrollingInteraction(
3174 CSSPoint::FromAppUnits(distanceScrolled).Length());
3177 bool schedulePaint = true;
3178 if (nsLayoutUtils::AsyncPanZoomEnabled(this) &&
3179 !nsLayoutUtils::ShouldDisableApzForElement(content) &&
3180 !content->GetProperty(nsGkAtoms::MinimalDisplayPort) &&
3181 StaticPrefs::apz_paint_skipping_enabled()) {
3182 // If APZ is enabled with paint-skipping, there are certain conditions in
3183 // which we can skip paints:
3184 // 1) If APZ triggered this scroll, and the tile-aligned displayport is
3185 // unchanged.
3186 // 2) If non-APZ triggered this scroll, but we can handle it by just asking
3187 // APZ to update the scroll position. Again we make this conditional on
3188 // the tile-aligned displayport being unchanged.
3189 // We do the displayport check first since it's common to all scenarios,
3190 // and then if the displayport is unchanged, we check if APZ triggered,
3191 // or can handle, this scroll. If so, we set schedulePaint to false and
3192 // skip the paint.
3193 // Because of bug 1264297, we also don't do paint-skipping for elements with
3194 // perspective, because the displayport may not have captured everything
3195 // that needs to be painted. So even if the final tile-aligned displayport
3196 // is the same, we force a repaint for these elements. Bug 1254260 tracks
3197 // fixing this properly.
3198 nsRect displayPort;
3199 bool usingDisplayPort =
3200 DisplayPortUtils::GetDisplayPort(content, &displayPort);
3201 displayPort.MoveBy(-mScrolledFrame->GetPosition());
3203 PAINT_SKIP_LOG(
3204 "New scrollpos %s usingDP %d dpEqual %d scrollableByApz "
3205 "%d perspective %d bglocal %d filter %d\n",
3206 ToString(CSSPoint::FromAppUnits(GetScrollPosition())).c_str(),
3207 usingDisplayPort, displayPort.IsEqualEdges(oldDisplayPort),
3208 mScrollableByAPZ, HasPerspective(), HasBgAttachmentLocal(),
3209 mHasOutOfFlowContentInsideFilter);
3210 if (usingDisplayPort && displayPort.IsEqualEdges(oldDisplayPort) &&
3211 !HasPerspective() && !HasBgAttachmentLocal() &&
3212 !mHasOutOfFlowContentInsideFilter) {
3213 bool haveScrollLinkedEffects =
3214 content->GetComposedDoc()->HasScrollLinkedEffect();
3215 bool apzDisabled = haveScrollLinkedEffects &&
3216 StaticPrefs::apz_disable_for_scroll_linked_effects();
3217 if (!apzDisabled) {
3218 if (LastScrollOrigin() == ScrollOrigin::Apz) {
3219 schedulePaint = false;
3220 PAINT_SKIP_LOG("Skipping due to APZ scroll\n");
3221 } else if (mScrollableByAPZ) {
3222 nsIWidget* widget = presContext->GetNearestWidget();
3223 WindowRenderer* renderer =
3224 widget ? widget->GetWindowRenderer() : nullptr;
3225 if (renderer) {
3226 mozilla::layers::ScrollableLayerGuid::ViewID id;
3227 bool success = nsLayoutUtils::FindIDFor(content, &id);
3228 MOZ_ASSERT(success); // we have a displayport, we better have an ID
3230 // Schedule an empty transaction to carry over the scroll offset
3231 // update, instead of a full transaction. This empty transaction
3232 // might still get squashed into a full transaction if something
3233 // happens to trigger one.
3234 MOZ_ASSERT(!mScrollUpdates.IsEmpty());
3235 success = renderer->AddPendingScrollUpdateForNextTransaction(
3236 id, mScrollUpdates.LastElement());
3237 if (success) {
3238 schedulePaint = false;
3239 SchedulePaint(nsIFrame::PAINT_COMPOSITE_ONLY);
3240 PAINT_SKIP_LOG(
3241 "Skipping due to APZ-forwarded main-thread scroll\n");
3242 } else {
3243 PAINT_SKIP_LOG(
3244 "Failed to set pending scroll update on layer manager\n");
3252 // If the new scroll offset is going to clobber APZ's scroll offset, for
3253 // the RCD-RSF this will have the effect of updating the visual viewport
3254 // offset in a way that keeps the relative offset between the layout and
3255 // visual viewports constant. This will cause APZ to send us a new visual
3256 // viewport offset, but instead of waiting for that, just set the value
3257 // we expect APZ will set ourselves, to minimize the chances of
3258 // inconsistencies from querying a stale value.
3259 if (mIsRoot && nsLayoutUtils::CanScrollOriginClobberApz(aOrigin)) {
3260 AutoWeakFrame weakFrame(this);
3261 AutoScrollbarRepaintSuppression repaintSuppression(this, weakFrame,
3262 !schedulePaint);
3264 nsPoint visualViewportOffset = curPos;
3265 if (presContext->PresShell()->IsVisualViewportOffsetSet()) {
3266 visualViewportOffset =
3267 presContext->PresShell()->GetVisualViewportOffset();
3269 nsPoint relativeOffset = visualViewportOffset - curPos;
3271 presContext->PresShell()->SetVisualViewportOffset(pt + relativeOffset,
3272 curPos);
3273 if (!weakFrame.IsAlive()) {
3274 return;
3278 if (schedulePaint) {
3279 SchedulePaint();
3281 if (needFrameVisibilityUpdate) {
3282 presContext->PresShell()->ScheduleApproximateFrameVisibilityUpdateNow();
3286 if (ChildrenHavePerspective()) {
3287 // The overflow areas of descendants may depend on the scroll position,
3288 // so ensure they get updated.
3290 // First we recompute the overflow areas of the transformed children
3291 // that use the perspective. FinishAndStoreOverflow only calls this
3292 // if the size changes, so we need to do it manually.
3293 RecomputePerspectiveChildrenOverflow(this);
3295 // Update the overflow for the scrolled frame to take any changes from the
3296 // children into account.
3297 mScrolledFrame->UpdateOverflow();
3299 // Update the overflow for the outer so that we recompute scrollbars.
3300 UpdateOverflow();
3303 ScheduleSyntheticMouseMove();
3305 nsAutoScriptBlocker scriptBlocker;
3306 PresShell::AutoAssertNoFlush noFlush(*PresShell());
3308 { // scope the AutoScrollbarRepaintSuppression
3309 AutoWeakFrame weakFrame(this);
3310 AutoScrollbarRepaintSuppression repaintSuppression(this, weakFrame,
3311 !schedulePaint);
3312 UpdateScrollbarPosition();
3313 if (!weakFrame.IsAlive()) {
3314 return;
3318 presContext->RecordInteractionTime(
3319 nsPresContext::InteractionType::ScrollInteraction, TimeStamp::Now());
3321 PostScrollEvent();
3322 // If this is a viewport scroll, this could affect the relative offset
3323 // between layout and visual viewport, so we might have to fire a visual
3324 // viewport scroll event as well.
3325 if (mIsRoot) {
3326 if (auto* window = nsGlobalWindowInner::Cast(
3327 PresContext()->Document()->GetInnerWindow())) {
3328 window->VisualViewport()->PostScrollEvent(
3329 presContext->PresShell()->GetVisualViewportOffset(), curPos);
3333 // Schedule the scroll-timelines linked to its scrollable frame.
3334 // if `pt == curPos`, we early return, so the position must be changed at
3335 // this moment. Therefore, we can schedule scroll animations directly.
3336 ScheduleScrollAnimations();
3338 // notify the listeners.
3339 for (uint32_t i = 0; i < mListeners.Length(); i++) {
3340 mListeners[i]->ScrollPositionDidChange(pt.x, pt.y);
3343 if (nsCOMPtr<nsIDocShell> docShell = presContext->GetDocShell()) {
3344 docShell->NotifyScrollObservers();
3348 // Finds the max z-index of the items in aList that meet the following
3349 // conditions
3350 // 1) have z-index auto or z-index >= 0, and
3351 // 2) aFrame is a proper ancestor of the item's frame.
3352 // Returns Nothing() if there is no such item.
3353 static Maybe<int32_t> MaxZIndexInListOfItemsContainedInFrame(
3354 nsDisplayList* aList, nsIFrame* aFrame) {
3355 Maybe<int32_t> maxZIndex = Nothing();
3356 for (nsDisplayItem* item : *aList) {
3357 int32_t zIndex = item->ZIndex();
3358 if (zIndex < 0 ||
3359 !nsLayoutUtils::IsProperAncestorFrame(aFrame, item->Frame())) {
3360 continue;
3362 if (!maxZIndex) {
3363 maxZIndex = Some(zIndex);
3364 } else {
3365 maxZIndex = Some(std::max(maxZIndex.value(), zIndex));
3368 return maxZIndex;
3371 template <class T>
3372 static void AppendInternalItemToTop(const nsDisplayListSet& aLists, T* aItem,
3373 const Maybe<int32_t>& aZIndex) {
3374 if (aZIndex) {
3375 aItem->SetOverrideZIndex(aZIndex.value());
3376 aLists.PositionedDescendants()->AppendToTop(aItem);
3377 } else {
3378 aLists.Content()->AppendToTop(aItem);
3382 static const uint32_t APPEND_OWN_LAYER = 0x1;
3383 static const uint32_t APPEND_POSITIONED = 0x2;
3384 static const uint32_t APPEND_SCROLLBAR_CONTAINER = 0x4;
3385 static const uint32_t APPEND_OVERLAY = 0x8;
3386 static const uint32_t APPEND_TOP = 0x10;
3388 static void AppendToTop(nsDisplayListBuilder* aBuilder,
3389 const nsDisplayListSet& aLists, nsDisplayList* aSource,
3390 nsIFrame* aSourceFrame, nsIFrame* aScrollFrame,
3391 uint32_t aFlags) {
3392 if (aSource->IsEmpty()) {
3393 return;
3396 nsDisplayWrapList* newItem;
3397 const ActiveScrolledRoot* asr = aBuilder->CurrentActiveScrolledRoot();
3398 if (aFlags & APPEND_OWN_LAYER) {
3399 ScrollbarData scrollbarData;
3400 if (aFlags & APPEND_SCROLLBAR_CONTAINER) {
3401 scrollbarData = ScrollbarData::CreateForScrollbarContainer(
3402 aBuilder->GetCurrentScrollbarDirection(),
3403 aBuilder->GetCurrentScrollbarTarget());
3404 // Direction should be set
3405 MOZ_ASSERT(scrollbarData.mDirection.isSome());
3408 newItem = MakeDisplayItemWithIndex<nsDisplayOwnLayer>(
3409 aBuilder, aSourceFrame,
3410 /* aIndex = */ nsDisplayOwnLayer::OwnLayerForScrollbar, aSource, asr,
3411 nsDisplayOwnLayerFlags::None, scrollbarData, true, false);
3412 } else {
3413 // Build the wrap list with an index of 1, since the scrollbar frame itself
3414 // might have already built an nsDisplayWrapList.
3415 newItem = MakeDisplayItemWithIndex<nsDisplayWrapper>(
3416 aBuilder, aSourceFrame, 1, aSource, asr, false);
3418 if (!newItem) {
3419 return;
3422 if (aFlags & APPEND_POSITIONED) {
3423 // We want overlay scrollbars to always be on top of the scrolled content,
3424 // but we don't want them to unnecessarily cover overlapping elements from
3425 // outside our scroll frame.
3426 Maybe<int32_t> zIndex = Nothing();
3427 if (aFlags & APPEND_TOP) {
3428 zIndex = Some(INT32_MAX);
3429 } else if (aFlags & APPEND_OVERLAY) {
3430 zIndex = MaxZIndexInListOfItemsContainedInFrame(
3431 aLists.PositionedDescendants(), aScrollFrame);
3432 } else if (aSourceFrame->StylePosition()->mZIndex.IsInteger()) {
3433 zIndex = Some(aSourceFrame->StylePosition()->mZIndex.integer._0);
3435 AppendInternalItemToTop(aLists, newItem, zIndex);
3436 } else {
3437 aLists.BorderBackground()->AppendToTop(newItem);
3441 struct HoveredStateComparator {
3442 static bool Hovered(const nsIFrame* aFrame) {
3443 return aFrame->GetContent()->IsElement() &&
3444 aFrame->GetContent()->AsElement()->HasAttr(nsGkAtoms::hover);
3447 bool Equals(nsIFrame* A, nsIFrame* B) const {
3448 return Hovered(A) == Hovered(B);
3451 bool LessThan(nsIFrame* A, nsIFrame* B) const {
3452 return !Hovered(A) && Hovered(B);
3456 void nsHTMLScrollFrame::AppendScrollPartsTo(nsDisplayListBuilder* aBuilder,
3457 const nsDisplayListSet& aLists,
3458 bool aCreateLayer,
3459 bool aPositioned) {
3460 const bool overlayScrollbars = UsesOverlayScrollbars();
3462 AutoTArray<nsIFrame*, 3> scrollParts;
3463 for (nsIFrame* kid : PrincipalChildList()) {
3464 if (kid == mScrolledFrame ||
3465 (kid->IsAbsPosContainingBlock() || overlayScrollbars) != aPositioned) {
3466 continue;
3469 scrollParts.AppendElement(kid);
3471 if (scrollParts.IsEmpty()) {
3472 return;
3475 // We can't check will-change budget during display list building phase.
3476 // This means that we will build scroll bar layers for out of budget
3477 // will-change: scroll position.
3478 const mozilla::layers::ScrollableLayerGuid::ViewID scrollTargetId =
3479 IsScrollingActive()
3480 ? nsLayoutUtils::FindOrCreateIDFor(mScrolledFrame->GetContent())
3481 : mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID;
3483 scrollParts.Sort(HoveredStateComparator());
3485 DisplayListClipState::AutoSaveRestore clipState(aBuilder);
3486 // Don't let scrollparts extent outside our frame's border-box, if these are
3487 // viewport scrollbars. They would create layerization problems. This wouldn't
3488 // normally be an issue but themes can add overflow areas to scrollbar parts.
3489 if (mIsRoot) {
3490 nsRect scrollPartsClip(aBuilder->ToReferenceFrame(this),
3491 TrueOuterSize(aBuilder));
3492 clipState.ClipContentDescendants(scrollPartsClip);
3495 for (uint32_t i = 0; i < scrollParts.Length(); ++i) {
3496 MOZ_ASSERT(scrollParts[i]);
3497 Maybe<ScrollDirection> scrollDirection;
3498 uint32_t appendToTopFlags = 0;
3499 if (scrollParts[i] == mVScrollbarBox) {
3500 scrollDirection.emplace(ScrollDirection::eVertical);
3501 appendToTopFlags |= APPEND_SCROLLBAR_CONTAINER;
3503 if (scrollParts[i] == mHScrollbarBox) {
3504 MOZ_ASSERT(!scrollDirection.isSome());
3505 scrollDirection.emplace(ScrollDirection::eHorizontal);
3506 appendToTopFlags |= APPEND_SCROLLBAR_CONTAINER;
3509 // The display port doesn't necessarily include the scrollbars, so just
3510 // include all of the scrollbars if we are in a RCD-RSF. We only do
3511 // this for the root scrollframe of the root content document, which is
3512 // zoomable, and where the scrollbar sizes are bounded by the widget.
3513 const nsRect visible =
3514 mIsRoot && PresContext()->IsRootContentDocumentCrossProcess()
3515 ? scrollParts[i]->InkOverflowRectRelativeToParent()
3516 : aBuilder->GetVisibleRect();
3517 if (visible.IsEmpty()) {
3518 continue;
3520 const nsRect dirty =
3521 mIsRoot && PresContext()->IsRootContentDocumentCrossProcess()
3522 ? scrollParts[i]->InkOverflowRectRelativeToParent()
3523 : aBuilder->GetDirtyRect();
3525 // Always create layers for overlay scrollbars so that we don't create a
3526 // giant layer covering the whole scrollport if both scrollbars are visible.
3527 const bool isOverlayScrollbar =
3528 scrollDirection.isSome() && overlayScrollbars;
3529 const bool createLayer =
3530 aCreateLayer || isOverlayScrollbar ||
3531 StaticPrefs::layout_scrollbars_always_layerize_track();
3533 nsDisplayListCollection partList(aBuilder);
3535 nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
3536 aBuilder, this, visible, dirty);
3538 nsDisplayListBuilder::AutoCurrentScrollbarInfoSetter infoSetter(
3539 aBuilder, scrollTargetId, scrollDirection, createLayer);
3540 BuildDisplayListForChild(
3541 aBuilder, scrollParts[i], partList,
3542 nsIFrame::DisplayChildFlag::ForceStackingContext);
3545 // DisplayChildFlag::ForceStackingContext put everything into
3546 // partList.PositionedDescendants().
3547 if (partList.PositionedDescendants()->IsEmpty()) {
3548 continue;
3551 if (createLayer) {
3552 appendToTopFlags |= APPEND_OWN_LAYER;
3554 if (aPositioned) {
3555 appendToTopFlags |= APPEND_POSITIONED;
3558 if (isOverlayScrollbar || scrollParts[i] == mResizerBox) {
3559 if (isOverlayScrollbar && mIsRoot) {
3560 appendToTopFlags |= APPEND_TOP;
3561 } else {
3562 appendToTopFlags |= APPEND_OVERLAY;
3563 aBuilder->SetDisablePartialUpdates(true);
3568 nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
3569 aBuilder, scrollParts[i], visible + GetOffsetTo(scrollParts[i]),
3570 dirty + GetOffsetTo(scrollParts[i]));
3571 if (scrollParts[i]->IsTransformed()) {
3572 nsPoint toOuterReferenceFrame;
3573 const nsIFrame* outerReferenceFrame = aBuilder->FindReferenceFrameFor(
3574 scrollParts[i]->GetParent(), &toOuterReferenceFrame);
3575 toOuterReferenceFrame += scrollParts[i]->GetPosition();
3577 buildingForChild.SetReferenceFrameAndCurrentOffset(
3578 outerReferenceFrame, toOuterReferenceFrame);
3580 nsDisplayListBuilder::AutoCurrentScrollbarInfoSetter infoSetter(
3581 aBuilder, scrollTargetId, scrollDirection, createLayer);
3583 ::AppendToTop(aBuilder, aLists, partList.PositionedDescendants(),
3584 scrollParts[i], this, appendToTopFlags);
3589 nsRect nsHTMLScrollFrame::ExpandRectToNearlyVisible(const nsRect& aRect) const {
3590 // We don't want to expand a rect in a direction that we can't scroll, so we
3591 // check the scroll range.
3592 nsRect scrollRange = GetLayoutScrollRange();
3593 nsPoint scrollPos = GetScrollPosition();
3594 nsMargin expand(0, 0, 0, 0);
3596 nscoord vertShift =
3597 StaticPrefs::layout_framevisibility_numscrollportheights() * aRect.height;
3598 if (scrollRange.y < scrollPos.y) {
3599 expand.top = vertShift;
3601 if (scrollPos.y < scrollRange.YMost()) {
3602 expand.bottom = vertShift;
3605 nscoord horzShift =
3606 StaticPrefs::layout_framevisibility_numscrollportwidths() * aRect.width;
3607 if (scrollRange.x < scrollPos.x) {
3608 expand.left = horzShift;
3610 if (scrollPos.x < scrollRange.XMost()) {
3611 expand.right = horzShift;
3614 nsRect rect = aRect;
3615 rect.Inflate(expand);
3616 return rect;
3619 static bool ShouldBeClippedByFrame(nsIFrame* aClipFrame,
3620 nsIFrame* aClippedFrame) {
3621 return nsLayoutUtils::IsProperAncestorFrame(aClipFrame, aClippedFrame);
3624 static void ClipItemsExceptCaret(
3625 nsDisplayList* aList, nsDisplayListBuilder* aBuilder, nsIFrame* aClipFrame,
3626 const DisplayItemClipChain* aExtraClip,
3627 nsTHashMap<nsPtrHashKey<const DisplayItemClipChain>,
3628 const DisplayItemClipChain*>& aCache) {
3629 for (nsDisplayItem* i : *aList) {
3630 if (!ShouldBeClippedByFrame(aClipFrame, i->Frame())) {
3631 continue;
3634 const DisplayItemType type = i->GetType();
3635 if (type != DisplayItemType::TYPE_CARET &&
3636 type != DisplayItemType::TYPE_CONTAINER) {
3637 const DisplayItemClipChain* clip = i->GetClipChain();
3638 const DisplayItemClipChain* intersection = nullptr;
3639 if (aCache.Get(clip, &intersection)) {
3640 i->SetClipChain(intersection, true);
3641 } else {
3642 i->IntersectClip(aBuilder, aExtraClip, true);
3643 aCache.InsertOrUpdate(clip, i->GetClipChain());
3646 nsDisplayList* children = i->GetSameCoordinateSystemChildren();
3647 if (children) {
3648 ClipItemsExceptCaret(children, aBuilder, aClipFrame, aExtraClip, aCache);
3653 static void ClipListsExceptCaret(nsDisplayListCollection* aLists,
3654 nsDisplayListBuilder* aBuilder,
3655 nsIFrame* aClipFrame,
3656 const DisplayItemClipChain* aExtraClip) {
3657 nsTHashMap<nsPtrHashKey<const DisplayItemClipChain>,
3658 const DisplayItemClipChain*>
3659 cache;
3660 ClipItemsExceptCaret(aLists->BorderBackground(), aBuilder, aClipFrame,
3661 aExtraClip, cache);
3662 ClipItemsExceptCaret(aLists->BlockBorderBackgrounds(), aBuilder, aClipFrame,
3663 aExtraClip, cache);
3664 ClipItemsExceptCaret(aLists->Floats(), aBuilder, aClipFrame, aExtraClip,
3665 cache);
3666 ClipItemsExceptCaret(aLists->PositionedDescendants(), aBuilder, aClipFrame,
3667 aExtraClip, cache);
3668 ClipItemsExceptCaret(aLists->Outlines(), aBuilder, aClipFrame, aExtraClip,
3669 cache);
3670 ClipItemsExceptCaret(aLists->Content(), aBuilder, aClipFrame, aExtraClip,
3671 cache);
3674 // This is similar to a "save-restore" RAII class for
3675 // DisplayListBuilder::ContainsBlendMode(), with a slight enhancement.
3676 // If this class is put on the stack and then unwound, the DL builder's
3677 // ContainsBlendMode flag will be effectively the same as if this class wasn't
3678 // put on the stack. However, if the CaptureContainsBlendMode method is called,
3679 // there will be a difference - the blend mode in the descendant display lists
3680 // will be "captured" and extracted.
3681 // The main goal here is to allow conditionally capturing the flag that
3682 // indicates whether or not a blend mode was encountered in the descendant part
3683 // of the display list.
3684 class MOZ_RAII AutoContainsBlendModeCapturer {
3685 nsDisplayListBuilder& mBuilder;
3686 bool mSavedContainsBlendMode;
3688 public:
3689 explicit AutoContainsBlendModeCapturer(nsDisplayListBuilder& aBuilder)
3690 : mBuilder(aBuilder),
3691 mSavedContainsBlendMode(aBuilder.ContainsBlendMode()) {
3692 mBuilder.SetContainsBlendMode(false);
3695 bool CaptureContainsBlendMode() {
3696 // "Capture" the flag by extracting and clearing the ContainsBlendMode flag
3697 // on the builder.
3698 bool capturedBlendMode = mBuilder.ContainsBlendMode();
3699 mBuilder.SetContainsBlendMode(false);
3700 return capturedBlendMode;
3703 ~AutoContainsBlendModeCapturer() {
3704 // If CaptureContainsBlendMode() was called, the descendant blend mode was
3705 // "captured" and so uncapturedContainsBlendMode will be false. If
3706 // CaptureContainsBlendMode() wasn't called, then no capture occurred, and
3707 // uncapturedContainsBlendMode may be true if there was a descendant blend
3708 // mode. In that case, we set the flag on the DL builder so that we restore
3709 // state to what it would have been without this RAII class on the stack.
3710 bool uncapturedContainsBlendMode = mBuilder.ContainsBlendMode();
3711 mBuilder.SetContainsBlendMode(mSavedContainsBlendMode ||
3712 uncapturedContainsBlendMode);
3716 void nsHTMLScrollFrame::MaybeCreateTopLayerAndWrapRootItems(
3717 nsDisplayListBuilder* aBuilder, nsDisplayListCollection& aSet,
3718 bool aCreateAsyncZoom,
3719 AutoContainsBlendModeCapturer* aAsyncZoomBlendCapture,
3720 const nsRect& aAsyncZoomClipRect, nscoord* aRadii) {
3721 if (!mIsRoot) {
3722 return;
3725 // Create any required items for the 'top layer' and check if they'll be
3726 // opaque over the entire area of the viewport. If they are, then we can
3727 // skip building display items for the rest of the page.
3728 if (ViewportFrame* viewport = do_QueryFrame(GetParent())) {
3729 bool topLayerIsOpaque = false;
3730 if (nsDisplayWrapList* topLayerWrapList =
3731 viewport->BuildDisplayListForTopLayer(aBuilder,
3732 &topLayerIsOpaque)) {
3733 // If the top layer content is opaque, and we're the root content document
3734 // in the process, we can drop the display items behind it. We only
3735 // support doing this for the root content document in the process, since
3736 // the top layer content might have fixed position items that have a
3737 // scrolltarget referencing the APZ data for the document. APZ builds this
3738 // data implicitly for the root content document in the process, but
3739 // subdocuments etc need their display items to generate it, so we can't
3740 // cull those.
3741 if (topLayerIsOpaque && PresContext()->IsRootContentDocumentInProcess()) {
3742 aSet.DeleteAll(aBuilder);
3744 aSet.PositionedDescendants()->AppendToTop(topLayerWrapList);
3748 nsDisplayList rootResultList(aBuilder);
3750 bool serializedList = false;
3751 auto SerializeList = [&] {
3752 if (!serializedList) {
3753 serializedList = true;
3754 aSet.SerializeWithCorrectZOrder(&rootResultList, GetContent());
3758 if (nsIFrame* rootStyleFrame = GetFrameForStyle()) {
3759 bool usingBackdropFilter =
3760 rootStyleFrame->StyleEffects()->HasBackdropFilters() &&
3761 rootStyleFrame->IsVisibleForPainting();
3763 if (rootStyleFrame->StyleEffects()->HasFilters() &&
3764 !aBuilder->IsForGenerateGlyphMask()) {
3765 SerializeList();
3766 rootResultList.AppendNewToTop<nsDisplayFilters>(
3767 aBuilder, this, &rootResultList, rootStyleFrame, usingBackdropFilter);
3770 if (usingBackdropFilter) {
3771 SerializeList();
3772 DisplayListClipState::AutoSaveRestore clipState(aBuilder);
3773 nsRect backdropRect =
3774 GetRectRelativeToSelf() + aBuilder->ToReferenceFrame(this);
3775 rootResultList.AppendNewToTop<nsDisplayBackdropFilters>(
3776 aBuilder, this, &rootResultList, backdropRect, rootStyleFrame);
3780 if (aCreateAsyncZoom) {
3781 MOZ_ASSERT(mIsRoot);
3783 // Wrap all our scrolled contents in an nsDisplayAsyncZoom. This will be
3784 // the layer that gets scaled for APZ zooming. It does not have the
3785 // scrolled ASR, but it does have the composition bounds clip applied to
3786 // it. The children have the layout viewport clip applied to them (above).
3787 // Effectively we are double clipping to the viewport, at potentially
3788 // different async scales.
3789 SerializeList();
3791 if (aAsyncZoomBlendCapture->CaptureContainsBlendMode()) {
3792 // The async zoom contents contain a mix-blend mode, so let's wrap all
3793 // those contents into a blend container, and then wrap the blend
3794 // container in the async zoom container. Otherwise the blend container
3795 // ends up outside the zoom container which results in blend failure for
3796 // WebRender.
3797 nsDisplayItem* blendContainer =
3798 nsDisplayBlendContainer::CreateForMixBlendMode(
3799 aBuilder, this, &rootResultList,
3800 aBuilder->CurrentActiveScrolledRoot());
3801 rootResultList.AppendToTop(blendContainer);
3803 // Blend containers can be created or omitted during partial updates
3804 // depending on the dirty rect. So we basically can't do partial updates
3805 // if there's a blend container involved. There is equivalent code to this
3806 // in the BuildDisplayListForStackingContext function as well, with a more
3807 // detailed comment explaining things better.
3808 if (aBuilder->IsRetainingDisplayList()) {
3809 if (aBuilder->IsPartialUpdate()) {
3810 aBuilder->SetPartialBuildFailed(true);
3811 } else {
3812 aBuilder->SetDisablePartialUpdates(true);
3817 mozilla::layers::FrameMetrics::ViewID viewID =
3818 nsLayoutUtils::FindOrCreateIDFor(mScrolledFrame->GetContent());
3820 DisplayListClipState::AutoSaveRestore clipState(aBuilder);
3821 clipState.ClipContentDescendants(aAsyncZoomClipRect, aRadii);
3823 rootResultList.AppendNewToTop<nsDisplayAsyncZoom>(
3824 aBuilder, this, &rootResultList, aBuilder->CurrentActiveScrolledRoot(),
3825 viewID);
3828 if (serializedList) {
3829 aSet.Content()->AppendToTop(&rootResultList);
3833 void nsHTMLScrollFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
3834 const nsDisplayListSet& aLists) {
3835 SetAndNullOnExit<const nsIFrame> tmpBuilder(
3836 mReferenceFrameDuringPainting, aBuilder->GetCurrentReferenceFrame());
3837 if (aBuilder->IsForFrameVisibility()) {
3838 NotifyApproximateFrameVisibilityUpdate(false);
3841 DisplayBorderBackgroundOutline(aBuilder, aLists);
3843 const bool isRootContent =
3844 mIsRoot && PresContext()->IsRootContentDocumentCrossProcess();
3846 nsRect effectiveScrollPort = mScrollPort;
3847 if (isRootContent && PresContext()->HasDynamicToolbar()) {
3848 // Expand the scroll port to the size including the area covered by dynamic
3849 // toolbar in the case where the dynamic toolbar is being used since
3850 // position:fixed elements attached to this root scroller might be taller
3851 // than its scroll port (e.g 100vh). Even if the dynamic toolbar covers the
3852 // taller area, it doesn't mean the area is clipped by the toolbar because
3853 // the dynamic toolbar is laid out outside of our topmost window and it
3854 // transitions without changing our topmost window size.
3855 effectiveScrollPort.SizeTo(nsLayoutUtils::ExpandHeightForDynamicToolbar(
3856 PresContext(), effectiveScrollPort.Size()));
3859 // It's safe to get this value before the DecideScrollableLayer call below
3860 // because that call cannot create a displayport for root scroll frames,
3861 // and hence it cannot create an ignore scroll frame.
3862 const bool ignoringThisScrollFrame = aBuilder->GetIgnoreScrollFrame() == this;
3864 // Overflow clipping can never clip frames outside our subtree, so there
3865 // is no need to worry about whether we are a moving frame that might clip
3866 // non-moving frames.
3867 // Not all our descendants will be clipped by overflow clipping, but all
3868 // the ones that aren't clipped will be out of flow frames that have already
3869 // had dirty rects saved for them by their parent frames calling
3870 // MarkOutOfFlowChildrenForDisplayList, so it's safe to restrict our
3871 // dirty rect here.
3872 nsRect visibleRect = aBuilder->GetVisibleRect();
3873 nsRect dirtyRect = aBuilder->GetDirtyRect();
3874 if (!ignoringThisScrollFrame) {
3875 visibleRect = visibleRect.Intersect(effectiveScrollPort);
3876 dirtyRect = dirtyRect.Intersect(effectiveScrollPort);
3879 bool dirtyRectHasBeenOverriden = false;
3880 Unused << DecideScrollableLayer(aBuilder, &visibleRect, &dirtyRect,
3881 /* aSetBase = */ !mIsRoot,
3882 &dirtyRectHasBeenOverriden);
3884 if (aBuilder->IsForFrameVisibility()) {
3885 // We expand the dirty rect to catch frames just outside of the scroll port.
3886 // We use the dirty rect instead of the whole scroll port to prevent
3887 // too much expansion in the presence of very large (bigger than the
3888 // viewport) scroll ports.
3889 dirtyRect = ExpandRectToNearlyVisible(dirtyRect);
3890 visibleRect = dirtyRect;
3893 // We put non-overlay scrollbars in their own layers when this is the root
3894 // scroll frame and we are a toplevel content document. In this situation,
3895 // the scrollbar(s) would normally be assigned their own layer anyway, since
3896 // they're not scrolled with the rest of the document. But when both
3897 // scrollbars are visible, the layer's visible rectangle would be the size
3898 // of the viewport, so most layer implementations would create a layer buffer
3899 // that's much larger than necessary. Creating independent layers for each
3900 // scrollbar works around the problem.
3901 const bool createLayersForScrollbars = isRootContent;
3903 nsDisplayListCollection set(aBuilder);
3905 if (ignoringThisScrollFrame) {
3906 // If we are a root scroll frame that has a display port we want to add
3907 // scrollbars, they will be children of the scrollable layer, but they get
3908 // adjusted by the APZC automatically.
3909 bool addScrollBars =
3910 mIsRoot && mWillBuildScrollableLayer && aBuilder->IsPaintingToWindow();
3912 if (addScrollBars) {
3913 // Add classic scrollbars.
3914 AppendScrollPartsTo(aBuilder, set, createLayersForScrollbars, false);
3918 nsDisplayListBuilder::AutoBuildingDisplayList building(
3919 aBuilder, this, visibleRect, dirtyRect);
3921 // Don't clip the scrolled child, and don't paint scrollbars/scrollcorner.
3922 // The scrolled frame shouldn't have its own background/border, so we
3923 // can just pass aLists directly.
3924 BuildDisplayListForChild(aBuilder, mScrolledFrame, set);
3927 MaybeCreateTopLayerAndWrapRootItems(aBuilder, set,
3928 /* aCreateAsyncZoom = */ false, nullptr,
3929 nsRect(), nullptr);
3931 if (addScrollBars) {
3932 // Add overlay scrollbars.
3933 AppendScrollPartsTo(aBuilder, set, createLayersForScrollbars, true);
3936 set.MoveTo(aLists);
3937 return;
3940 // Whether we might want to build a scrollable layer for this scroll frame
3941 // at some point in the future. This controls whether we add the information
3942 // to the layer tree (a scroll info layer if necessary, and add the right
3943 // area to the dispatch to content layer event regions) necessary to activate
3944 // a scroll frame so it creates a scrollable layer.
3945 const bool couldBuildLayer = [&] {
3946 if (!aBuilder->IsPaintingToWindow()) {
3947 return false;
3949 if (mWillBuildScrollableLayer) {
3950 return true;
3952 return StyleVisibility()->IsVisible() &&
3953 nsLayoutUtils::AsyncPanZoomEnabled(this) && WantAsyncScroll();
3954 }();
3956 // Now display the scrollbars and scrollcorner. These parts are drawn
3957 // in the border-background layer, on top of our own background and
3958 // borders and underneath borders and backgrounds of later elements
3959 // in the tree.
3960 // Note that this does not apply for overlay scrollbars; those are drawn
3961 // in the positioned-elements layer on top of everything else by the call
3962 // to AppendScrollPartsTo(..., true) further down.
3963 AppendScrollPartsTo(aBuilder, aLists, createLayersForScrollbars, false);
3965 const nsStyleDisplay* disp = StyleDisplay();
3966 if (aBuilder->IsForPainting() &&
3967 disp->mWillChange.bits & StyleWillChangeBits::SCROLL) {
3968 aBuilder->AddToWillChangeBudget(this, GetVisualViewportSize());
3971 mScrollParentID = aBuilder->GetCurrentScrollParentId();
3973 Maybe<nsRect> contentBoxClip;
3974 Maybe<const DisplayItemClipChain*> extraContentBoxClipForNonCaretContent;
3975 if (MOZ_UNLIKELY(
3976 disp->mOverflowClipBoxBlock == StyleOverflowClipBox::ContentBox ||
3977 disp->mOverflowClipBoxInline == StyleOverflowClipBox::ContentBox)) {
3978 WritingMode wm = mScrolledFrame->GetWritingMode();
3979 bool cbH = (wm.IsVertical() ? disp->mOverflowClipBoxBlock
3980 : disp->mOverflowClipBoxInline) ==
3981 StyleOverflowClipBox::ContentBox;
3982 bool cbV = (wm.IsVertical() ? disp->mOverflowClipBoxInline
3983 : disp->mOverflowClipBoxBlock) ==
3984 StyleOverflowClipBox::ContentBox;
3985 // We only clip if there is *scrollable* overflow, to avoid clipping
3986 // *ink* overflow unnecessarily.
3987 nsRect clipRect = effectiveScrollPort + aBuilder->ToReferenceFrame(this);
3988 nsMargin padding = GetUsedPadding();
3989 if (!cbH) {
3990 padding.left = padding.right = nscoord(0);
3992 if (!cbV) {
3993 padding.top = padding.bottom = nscoord(0);
3995 clipRect.Deflate(padding);
3997 nsRect so = mScrolledFrame->ScrollableOverflowRect();
3998 if ((cbH && (clipRect.width != so.width || so.x < 0)) ||
3999 (cbV && (clipRect.height != so.height || so.y < 0))) {
4000 // The non-inflated clip needs to be set on all non-caret items.
4001 // We prepare an extra DisplayItemClipChain here that will be intersected
4002 // with those items after they've been created.
4003 const ActiveScrolledRoot* asr = aBuilder->CurrentActiveScrolledRoot();
4005 DisplayItemClip newClip;
4006 newClip.SetTo(clipRect);
4008 const DisplayItemClipChain* extraClip =
4009 aBuilder->AllocateDisplayItemClipChain(newClip, asr, nullptr);
4011 extraContentBoxClipForNonCaretContent = Some(extraClip);
4013 nsIFrame* caretFrame = aBuilder->GetCaretFrame();
4014 // Avoid clipping it in a zero-height line box (heuristic only).
4015 if (caretFrame && caretFrame->GetRect().height != 0) {
4016 nsRect caretRect = aBuilder->GetCaretRect();
4017 // Allow the caret to stick out of the content box clip by half the
4018 // caret height on the top, and its full width on the right.
4019 nsRect inflatedClip = clipRect;
4020 inflatedClip.Inflate(
4021 nsMargin(caretRect.height / 2, caretRect.width, 0, 0));
4022 contentBoxClip = Some(inflatedClip);
4027 AutoContainsBlendModeCapturer blendCapture(*aBuilder);
4029 const bool willBuildAsyncZoomContainer =
4030 mWillBuildScrollableLayer && aBuilder->ShouldBuildAsyncZoomContainer() &&
4031 isRootContent;
4033 nsRect scrollPortClip =
4034 effectiveScrollPort + aBuilder->ToReferenceFrame(this);
4035 nsRect clipRect = scrollPortClip;
4036 // Our override of GetBorderRadii ensures we never have a radius at
4037 // the corners where we have a scrollbar.
4038 nscoord radii[8];
4039 const bool haveRadii = GetPaddingBoxBorderRadii(radii);
4040 if (mIsRoot) {
4041 clipRect.SizeTo(nsLayoutUtils::CalculateCompositionSizeForFrame(
4042 this, true /* aSubtractScrollbars */,
4043 nullptr /* aOverrideScrollPortSize */,
4044 // With the dynamic toolbar, this CalculateCompositionSizeForFrame call
4045 // basically expands the region being covered up by the dynamic toolbar,
4046 // but if the root scroll container is not scrollable, e.g. the root
4047 // element has `overflow: hidden` or `position: fixed`, the function
4048 // doesn't expand the region since expanding the region in such cases
4049 // will prevent the content from restoring zooming to 1.0 zoom level
4050 // such as bug 1652190. That said, this `clipRect` which will be used
4051 // for the async zoom container needs to be expanded because zoomed-in
4052 // contents can be scrollable __visually__ so that the region under the
4053 // dynamic toolbar area will be revealed.
4054 nsLayoutUtils::IncludeDynamicToolbar::Force));
4056 // The composition size is essentially in visual coordinates.
4057 // If we are hit-testing in layout coordinates, transform the clip rect
4058 // to layout coordinates to match.
4059 if (aBuilder->IsRelativeToLayoutViewport() && isRootContent) {
4060 clipRect = ViewportUtils::VisualToLayout(clipRect, PresShell());
4065 DisplayListClipState::AutoSaveRestore clipState(aBuilder);
4067 // If we're building an async zoom container, clip the contents inside
4068 // to the layout viewport (scrollPortClip). The composition bounds clip
4069 // (clipRect) will be applied to the zoom container itself in
4070 // MaybeCreateTopLayerAndWrapRootItems.
4071 nsRect clipRectForContents =
4072 willBuildAsyncZoomContainer ? scrollPortClip : clipRect;
4073 if (mIsRoot) {
4074 clipState.ClipContentDescendants(clipRectForContents,
4075 haveRadii ? radii : nullptr);
4076 } else {
4077 clipState.ClipContainingBlockDescendants(clipRectForContents,
4078 haveRadii ? radii : nullptr);
4081 Maybe<DisplayListClipState::AutoSaveRestore> contentBoxClipState;
4082 if (contentBoxClip) {
4083 contentBoxClipState.emplace(aBuilder);
4084 if (mIsRoot) {
4085 contentBoxClipState->ClipContentDescendants(*contentBoxClip);
4086 } else {
4087 contentBoxClipState->ClipContainingBlockDescendants(*contentBoxClip);
4091 nsDisplayListBuilder::AutoCurrentActiveScrolledRootSetter asrSetter(
4092 aBuilder);
4094 if (mWillBuildScrollableLayer && aBuilder->IsPaintingToWindow()) {
4095 // If this scroll frame has a first scrollable frame sequence number,
4096 // ensure that it matches the current paint sequence number. If it does
4097 // not, reset it so that we can expire the displayport. The stored
4098 // sequence number will not match that of the current paint if the dom
4099 // was mutated in some way that alters the order of scroll frames.
4100 if (IsFirstScrollableFrameSequenceNumber().isSome() &&
4101 *IsFirstScrollableFrameSequenceNumber() !=
4102 nsDisplayListBuilder::GetPaintSequenceNumber()) {
4103 SetIsFirstScrollableFrameSequenceNumber(Nothing());
4105 asrSetter.EnterScrollFrame(this);
4108 if (couldBuildLayer && mScrolledFrame->GetContent()) {
4109 asrSetter.SetCurrentScrollParentId(
4110 nsLayoutUtils::FindOrCreateIDFor(mScrolledFrame->GetContent()));
4113 if (mWillBuildScrollableLayer && aBuilder->BuildCompositorHitTestInfo()) {
4114 // Create a hit test info item for the scrolled content that's not
4115 // clipped to the displayport. This ensures that within the bounds
4116 // of the scroll frame, the scrolled content is always hit, even
4117 // if we are checkerboarding.
4118 CompositorHitTestInfo info =
4119 mScrolledFrame->GetCompositorHitTestInfo(aBuilder);
4121 if (info != CompositorHitTestInvisibleToHit) {
4122 auto* hitInfo =
4123 MakeDisplayItemWithIndex<nsDisplayCompositorHitTestInfo>(
4124 aBuilder, mScrolledFrame, 1);
4125 if (hitInfo) {
4126 aBuilder->SetCompositorHitTestInfo(info);
4127 set.BorderBackground()->AppendToTop(hitInfo);
4133 // Clip our contents to the unsnapped scrolled rect. This makes sure
4134 // that we don't have display items over the subpixel seam at the edge
4135 // of the scrolled area.
4136 DisplayListClipState::AutoSaveRestore scrolledRectClipState(aBuilder);
4137 nsRect scrolledRectClip =
4138 GetUnsnappedScrolledRectInternal(
4139 mScrolledFrame->ScrollableOverflowRect(), mScrollPort.Size()) +
4140 mScrolledFrame->GetPosition();
4141 bool clippedToDisplayPort = false;
4142 if (mWillBuildScrollableLayer && aBuilder->IsPaintingToWindow()) {
4143 // Clip the contents to the display port.
4144 // The dirty rect already acts kind of like a clip, in that
4145 // FrameLayerBuilder intersects item bounds and opaque regions with
4146 // it, but it doesn't have the consistent snapping behavior of a
4147 // true clip.
4148 // For a case where this makes a difference, imagine the following
4149 // scenario: The display port has an edge that falls on a fractional
4150 // layer pixel, and there's an opaque display item that covers the
4151 // whole display port up until that fractional edge, and there is a
4152 // transparent display item that overlaps the edge. We want to prevent
4153 // this transparent item from enlarging the scrolled layer's visible
4154 // region beyond its opaque region. The dirty rect doesn't do that -
4155 // it gets rounded out, whereas a true clip gets rounded to nearest
4156 // pixels.
4157 // If there is no display port, we don't need this because the clip
4158 // from the scroll port is still applied.
4159 scrolledRectClip = scrolledRectClip.Intersect(visibleRect);
4160 clippedToDisplayPort = scrolledRectClip.IsEqualEdges(visibleRect);
4162 scrolledRectClipState.ClipContainingBlockDescendants(
4163 scrolledRectClip + aBuilder->ToReferenceFrame(this));
4164 if (clippedToDisplayPort) {
4165 // We have to do this after the ClipContainingBlockDescendants call
4166 // above, otherwise that call will clobber the flag set by this call
4167 // to SetClippedToDisplayPort.
4168 scrolledRectClipState.SetClippedToDisplayPort();
4171 nsRect visibleRectForChildren = visibleRect;
4172 nsRect dirtyRectForChildren = dirtyRect;
4174 // If we are entering the RCD-RSF, we are crossing the async zoom
4175 // container boundary, and need to convert from visual to layout
4176 // coordinates.
4177 if (willBuildAsyncZoomContainer && aBuilder->IsForEventDelivery()) {
4178 MOZ_ASSERT(ViewportUtils::IsZoomedContentRoot(mScrolledFrame));
4179 visibleRectForChildren =
4180 ViewportUtils::VisualToLayout(visibleRectForChildren, PresShell());
4181 dirtyRectForChildren =
4182 ViewportUtils::VisualToLayout(dirtyRectForChildren, PresShell());
4185 nsDisplayListBuilder::AutoBuildingDisplayList building(
4186 aBuilder, this, visibleRectForChildren, dirtyRectForChildren);
4188 BuildDisplayListForChild(aBuilder, mScrolledFrame, set);
4190 if (dirtyRectHasBeenOverriden &&
4191 StaticPrefs::layout_display_list_show_rebuild_area()) {
4192 nsDisplaySolidColor* color = MakeDisplayItem<nsDisplaySolidColor>(
4193 aBuilder, this,
4194 dirtyRect + aBuilder->GetCurrentFrameOffsetToReferenceFrame(),
4195 NS_RGBA(0, 0, 255, 64), false);
4196 if (color) {
4197 color->SetOverrideZIndex(INT32_MAX);
4198 set.PositionedDescendants()->AppendToTop(color);
4203 if (extraContentBoxClipForNonCaretContent) {
4204 // The items were built while the inflated content box clip was in
4205 // effect, so that the caret wasn't clipped unnecessarily. We apply
4206 // the non-inflated clip to the non-caret items now, by intersecting
4207 // it with their existing clip.
4208 ClipListsExceptCaret(&set, aBuilder, mScrolledFrame,
4209 *extraContentBoxClipForNonCaretContent);
4212 if (aBuilder->IsPaintingToWindow()) {
4213 mIsParentToActiveScrollFrames =
4214 ShouldActivateAllScrollFrames()
4215 ? asrSetter.GetContainsNonMinimalDisplayPort()
4216 : asrSetter.ShouldForceLayerForScrollParent();
4219 if (asrSetter.ShouldForceLayerForScrollParent()) {
4220 // Note that forcing layerization of scroll parents follows the scroll
4221 // handoff chain which is subject to the out-of-flow-frames caveat noted
4222 // above (where the asrSetter variable is created).
4223 MOZ_ASSERT(couldBuildLayer && mScrolledFrame->GetContent() &&
4224 aBuilder->IsPaintingToWindow());
4225 if (!mWillBuildScrollableLayer) {
4226 // Set a displayport so next paint we don't have to force layerization
4227 // after the fact. It's ok to pass DoNotRepaint here, since we've
4228 // already painted the change and we're just optimizing it to be
4229 // detected earlier. We also won't confuse RetainedDisplayLists
4230 // with the silent change, since we explicitly request partial updates
4231 // to be disabled on the next paint.
4232 DisplayPortUtils::SetDisplayPortMargins(
4233 GetContent(), PresShell(), DisplayPortMargins::Empty(GetContent()),
4234 DisplayPortUtils::ClearMinimalDisplayPortProperty::Yes, 0,
4235 DisplayPortUtils::RepaintMode::DoNotRepaint);
4236 // Call DecideScrollableLayer to recompute mWillBuildScrollableLayer
4237 // and recompute the current animated geometry root if needed. It's
4238 // too late to change the dirty rect so pass a copy.
4239 nsRect copyOfDirtyRect = dirtyRect;
4240 nsRect copyOfVisibleRect = visibleRect;
4241 Unused << DecideScrollableLayer(aBuilder, &copyOfVisibleRect,
4242 &copyOfDirtyRect,
4243 /* aSetBase = */ false, nullptr);
4244 if (mWillBuildScrollableLayer) {
4245 #ifndef MOZ_WIDGET_ANDROID
4246 gfxCriticalNoteOnce << "inserted scroll frame";
4247 #endif
4248 asrSetter.InsertScrollFrame(this);
4249 aBuilder->SetDisablePartialUpdates(true);
4255 if (mWillBuildScrollableLayer && aBuilder->IsPaintingToWindow()) {
4256 aBuilder->ForceLayerForScrollParent();
4259 MaybeCreateTopLayerAndWrapRootItems(
4260 aBuilder, set, willBuildAsyncZoomContainer, &blendCapture, clipRect,
4261 haveRadii ? radii : nullptr);
4263 // We want to call SetContainsNonMinimalDisplayPort if
4264 // mWillBuildScrollableLayer is true for any reason other than having a
4265 // minimal display port.
4266 if (aBuilder->IsPaintingToWindow()) {
4267 if (DisplayPortUtils::HasNonMinimalDisplayPort(GetContent()) ||
4268 mZoomableByAPZ || nsContentUtils::HasScrollgrab(GetContent())) {
4269 aBuilder->SetContainsNonMinimalDisplayPort();
4273 if (couldBuildLayer) {
4274 CompositorHitTestInfo info(CompositorHitTestFlags::eVisibleToHitTest,
4275 CompositorHitTestFlags::eInactiveScrollframe);
4276 // If the scroll frame has non-default overscroll-behavior, instruct
4277 // APZ to require a target confirmation before processing events that
4278 // hit this scroll frame (that is, to drop the events if a
4279 // confirmation does not arrive within the timeout period). Otherwise,
4280 // APZ's fallback behaviour of scrolling the enclosing scroll frame
4281 // would violate the specified overscroll-behavior.
4282 auto overscroll = GetOverscrollBehaviorInfo();
4283 if (overscroll.mBehaviorX != OverscrollBehavior::Auto ||
4284 overscroll.mBehaviorY != OverscrollBehavior::Auto) {
4285 info += CompositorHitTestFlags::eRequiresTargetConfirmation;
4288 nsRect area = effectiveScrollPort + aBuilder->ToReferenceFrame(this);
4290 // Make sure that APZ will dispatch events back to content so we can
4291 // create a displayport for this frame. We'll add the item later on.
4292 if (!mWillBuildScrollableLayer && aBuilder->BuildCompositorHitTestInfo()) {
4293 // Make sure the z-index of the inactive item is at least zero.
4294 // Otherwise, it will end up behind non-positioned items in the scrolled
4295 // content.
4296 int32_t zIndex = MaxZIndexInListOfItemsContainedInFrame(
4297 set.PositionedDescendants(), this)
4298 .valueOr(0);
4299 if (aBuilder->IsPartialUpdate()) {
4300 for (nsDisplayItem* item : mScrolledFrame->DisplayItems()) {
4301 if (item->GetType() ==
4302 DisplayItemType::TYPE_COMPOSITOR_HITTEST_INFO) {
4303 auto* hitTestItem =
4304 static_cast<nsDisplayCompositorHitTestInfo*>(item);
4305 if (hitTestItem->GetHitTestInfo().Info().contains(
4306 CompositorHitTestFlags::eInactiveScrollframe)) {
4307 zIndex = std::max(zIndex, hitTestItem->ZIndex());
4308 item->SetCantBeReused();
4313 nsDisplayCompositorHitTestInfo* hitInfo =
4314 MakeDisplayItemWithIndex<nsDisplayCompositorHitTestInfo>(
4315 aBuilder, mScrolledFrame, 1, area, info);
4316 if (hitInfo) {
4317 AppendInternalItemToTop(set, hitInfo, Some(zIndex));
4318 aBuilder->SetCompositorHitTestInfo(info);
4322 if (aBuilder->ShouldBuildScrollInfoItemsForHoisting()) {
4323 aBuilder->AppendNewScrollInfoItemForHoisting(
4324 MakeDisplayItem<nsDisplayScrollInfoLayer>(aBuilder, mScrolledFrame,
4325 this, info, area));
4329 // Now display overlay scrollbars and the resizer, if we have one.
4330 AppendScrollPartsTo(aBuilder, set, createLayersForScrollbars, true);
4332 set.MoveTo(aLists);
4335 nsRect nsHTMLScrollFrame::RestrictToRootDisplayPort(
4336 const nsRect& aDisplayportBase) {
4337 // This function clips aDisplayportBase so that it is no larger than the
4338 // root frame's displayport (or the root composition bounds, if we can't
4339 // obtain the root frame's displayport). This is useful for ensuring that
4340 // the displayport of a tall scrollframe doesn't gobble up all the memory.
4342 nsPresContext* pc = PresContext();
4343 const nsPresContext* rootPresContext =
4344 pc->GetInProcessRootContentDocumentPresContext();
4345 if (!rootPresContext) {
4346 rootPresContext = pc->GetRootPresContext();
4348 if (!rootPresContext) {
4349 return aDisplayportBase;
4351 const mozilla::PresShell* const rootPresShell = rootPresContext->PresShell();
4352 nsIFrame* rootFrame = rootPresShell->GetRootScrollFrame();
4353 if (!rootFrame) {
4354 rootFrame = rootPresShell->GetRootFrame();
4356 if (!rootFrame) {
4357 return aDisplayportBase;
4360 // Make sure we aren't trying to restrict to our own displayport, which is a
4361 // circular dependency.
4362 MOZ_ASSERT(!mIsRoot || rootPresContext != pc);
4364 nsRect rootDisplayPort;
4365 bool hasDisplayPort =
4366 rootFrame->GetContent() && DisplayPortUtils::GetDisplayPort(
4367 rootFrame->GetContent(), &rootDisplayPort);
4368 if (hasDisplayPort) {
4369 // The display port of the root frame already factors in it's callback
4370 // transform, so subtract it out here, the GetCumulativeApzCallbackTransform
4371 // call below will add it back.
4372 MOZ_LOG(sDisplayportLog, LogLevel::Verbose,
4373 ("RestrictToRootDisplayPort: Existing root displayport is %s\n",
4374 ToString(rootDisplayPort).c_str()));
4375 if (nsIContent* content = rootFrame->GetContent()) {
4376 if (void* property =
4377 content->GetProperty(nsGkAtoms::apzCallbackTransform)) {
4378 rootDisplayPort -=
4379 CSSPoint::ToAppUnits(*static_cast<CSSPoint*>(property));
4382 } else {
4383 // If we don't have a display port on the root frame let's fall back to
4384 // the root composition bounds instead.
4385 nsRect rootCompBounds =
4386 nsRect(nsPoint(0, 0),
4387 nsLayoutUtils::CalculateCompositionSizeForFrame(rootFrame));
4389 // If rootFrame is the RCD-RSF then
4390 // CalculateCompositionSizeForFrame did not take the document's
4391 // resolution into account, so we must.
4392 if (rootPresContext->IsRootContentDocumentCrossProcess() &&
4393 rootFrame == rootPresShell->GetRootScrollFrame()) {
4394 MOZ_LOG(
4395 sDisplayportLog, LogLevel::Verbose,
4396 ("RestrictToRootDisplayPort: Removing resolution %f from root "
4397 "composition bounds %s\n",
4398 rootPresShell->GetResolution(), ToString(rootCompBounds).c_str()));
4399 rootCompBounds =
4400 rootCompBounds.RemoveResolution(rootPresShell->GetResolution());
4403 rootDisplayPort = rootCompBounds;
4405 MOZ_LOG(sDisplayportLog, LogLevel::Verbose,
4406 ("RestrictToRootDisplayPort: Intermediate root displayport %s\n",
4407 ToString(rootDisplayPort).c_str()));
4409 // We want to convert the root display port from the
4410 // coordinate space of |rootFrame| to the coordinate space of
4411 // |this|. We do that with the TransformRect call below.
4412 // However, since we care about the root display port
4413 // relative to what the user is actually seeing, we also need to
4414 // incorporate the APZ callback transforms into this. Most of the
4415 // time those transforms are negligible, but in some cases (e.g.
4416 // when a zoom is applied on an overflow:hidden document) it is
4417 // not (see bug 1280013).
4418 // XXX: Eventually we may want to create a modified version of
4419 // TransformRect that includes the APZ callback transforms
4420 // directly.
4421 nsLayoutUtils::TransformRect(rootFrame, this, rootDisplayPort);
4422 MOZ_LOG(sDisplayportLog, LogLevel::Verbose,
4423 ("RestrictToRootDisplayPort: Transformed root displayport %s\n",
4424 ToString(rootDisplayPort).c_str()));
4425 rootDisplayPort += CSSPoint::ToAppUnits(
4426 nsLayoutUtils::GetCumulativeApzCallbackTransform(this));
4427 MOZ_LOG(sDisplayportLog, LogLevel::Verbose,
4428 ("RestrictToRootDisplayPort: Final root displayport %s\n",
4429 ToString(rootDisplayPort).c_str()));
4431 // We want to limit aDisplayportBase to be no larger than
4432 // rootDisplayPort on either axis, but we don't want to just
4433 // blindly intersect the two, because rootDisplayPort might be
4434 // offset from where aDisplayportBase is (see bug 1327095 comment
4435 // 8). Instead, we translate rootDisplayPort so as to maximize the
4436 // overlap with aDisplayportBase, and *then* do the intersection.
4437 if (rootDisplayPort.x > aDisplayportBase.x &&
4438 rootDisplayPort.XMost() > aDisplayportBase.XMost()) {
4439 // rootDisplayPort is at a greater x-position for both left and
4440 // right, so translate it such that the XMost() values are the
4441 // same. This will line up the right edge of the two rects, and
4442 // might mean that rootDisplayPort.x is smaller than
4443 // aDisplayportBase.x. We can avoid that by taking the min of the
4444 // x delta and XMost() delta, but it doesn't really matter
4445 // because the intersection between the two rects below will end
4446 // up the same.
4447 rootDisplayPort.x -= (rootDisplayPort.XMost() - aDisplayportBase.XMost());
4448 } else if (rootDisplayPort.x < aDisplayportBase.x &&
4449 rootDisplayPort.XMost() < aDisplayportBase.XMost()) {
4450 // Analaogous code for when the rootDisplayPort is at a smaller
4451 // x-position.
4452 rootDisplayPort.x = aDisplayportBase.x;
4454 // Do the same for y-axis
4455 if (rootDisplayPort.y > aDisplayportBase.y &&
4456 rootDisplayPort.YMost() > aDisplayportBase.YMost()) {
4457 rootDisplayPort.y -= (rootDisplayPort.YMost() - aDisplayportBase.YMost());
4458 } else if (rootDisplayPort.y < aDisplayportBase.y &&
4459 rootDisplayPort.YMost() < aDisplayportBase.YMost()) {
4460 rootDisplayPort.y = aDisplayportBase.y;
4462 MOZ_LOG(
4463 sDisplayportLog, LogLevel::Verbose,
4464 ("RestrictToRootDisplayPort: Root displayport translated to %s to "
4465 "better enclose %s\n",
4466 ToString(rootDisplayPort).c_str(), ToString(aDisplayportBase).c_str()));
4468 // Now we can do the intersection
4469 return aDisplayportBase.Intersect(rootDisplayPort);
4472 /* static */ bool nsHTMLScrollFrame::ShouldActivateAllScrollFrames() {
4473 return (StaticPrefs::apz_wr_activate_all_scroll_frames() ||
4474 (StaticPrefs::apz_wr_activate_all_scroll_frames_when_fission() &&
4475 FissionAutostart()));
4478 bool nsHTMLScrollFrame::DecideScrollableLayer(
4479 nsDisplayListBuilder* aBuilder, nsRect* aVisibleRect, nsRect* aDirtyRect,
4480 bool aSetBase, bool* aDirtyRectHasBeenOverriden) {
4481 nsIContent* content = GetContent();
4482 bool hasDisplayPort = DisplayPortUtils::HasDisplayPort(content);
4483 // For hit testing purposes with fission we want to create a
4484 // minimal display port for every scroll frame that could be active. (We only
4485 // do this when aSetBase is true because we only want to do this the first
4486 // time this function is called for the same scroll frame.)
4487 if (aSetBase && !hasDisplayPort && aBuilder->IsPaintingToWindow() &&
4488 ShouldActivateAllScrollFrames() &&
4489 nsLayoutUtils::AsyncPanZoomEnabled(this) && WantAsyncScroll()) {
4490 // SetDisplayPortMargins calls TriggerDisplayPortExpiration which starts a
4491 // display port expiry timer for display ports that do expire. However
4492 // minimal display ports do not expire, so the display port has to be
4493 // marked before the SetDisplayPortMargins call so the expiry timer
4494 // doesn't get started.
4495 content->SetProperty(nsGkAtoms::MinimalDisplayPort,
4496 reinterpret_cast<void*>(true));
4498 DisplayPortUtils::SetDisplayPortMargins(
4499 content, PresShell(), DisplayPortMargins::Empty(content),
4500 DisplayPortUtils::ClearMinimalDisplayPortProperty::No, 0,
4501 DisplayPortUtils::RepaintMode::DoNotRepaint);
4502 hasDisplayPort = true;
4505 if (aBuilder->IsPaintingToWindow()) {
4506 if (aSetBase) {
4507 nsRect displayportBase = *aVisibleRect;
4508 nsPresContext* pc = PresContext();
4510 bool isChromeRootDoc =
4511 !pc->Document()->IsContentDocument() && !pc->GetParentPresContext();
4513 if (mIsRoot &&
4514 (pc->IsRootContentDocumentCrossProcess() || isChromeRootDoc)) {
4515 displayportBase =
4516 nsRect(nsPoint(0, 0),
4517 nsLayoutUtils::CalculateCompositionSizeForFrame(this));
4518 } else {
4519 // Make the displayport base equal to the visible rect restricted to
4520 // the scrollport and the root composition bounds, relative to the
4521 // scrollport.
4522 displayportBase = aVisibleRect->Intersect(mScrollPort);
4524 mozilla::layers::ScrollableLayerGuid::ViewID viewID =
4525 mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID;
4526 if (MOZ_LOG_TEST(sDisplayportLog, LogLevel::Verbose)) {
4527 nsLayoutUtils::FindIDFor(GetContent(), &viewID);
4528 MOZ_LOG(
4529 sDisplayportLog, LogLevel::Verbose,
4530 ("Scroll id %" PRIu64 " has visible rect %s, scroll port %s\n",
4531 viewID, ToString(*aVisibleRect).c_str(),
4532 ToString(mScrollPort).c_str()));
4535 // Only restrict to the root displayport bounds if necessary,
4536 // as the required coordinate transformation is expensive.
4537 // And don't call RestrictToRootDisplayPort if we would be trying to
4538 // restrict to our own display port, which doesn't make sense (ie if we
4539 // are a root scroll frame in a process root prescontext).
4540 if (hasDisplayPort && (!mIsRoot || pc->GetParentPresContext()) &&
4541 !DisplayPortUtils::WillUseEmptyDisplayPortMargins(content)) {
4542 displayportBase = RestrictToRootDisplayPort(displayportBase);
4543 MOZ_LOG(sDisplayportLog, LogLevel::Verbose,
4544 ("Scroll id %" PRIu64 " has restricted base %s\n", viewID,
4545 ToString(displayportBase).c_str()));
4547 displayportBase -= mScrollPort.TopLeft();
4550 DisplayPortUtils::SetDisplayPortBase(GetContent(), displayportBase);
4553 // If we don't have aSetBase == true then should have already
4554 // been called with aSetBase == true which should have set a
4555 // displayport base.
4556 MOZ_ASSERT(content->GetProperty(nsGkAtoms::DisplayPortBase));
4557 nsRect displayPort;
4558 hasDisplayPort = DisplayPortUtils::GetDisplayPort(
4559 content, &displayPort,
4560 DisplayPortOptions().With(DisplayportRelativeTo::ScrollFrame));
4562 auto OverrideDirtyRect = [&](const nsRect& aRect) {
4563 *aDirtyRect = aRect;
4564 if (aDirtyRectHasBeenOverriden) {
4565 *aDirtyRectHasBeenOverriden = true;
4569 if (hasDisplayPort) {
4570 // Override the dirty rectangle if the displayport has been set.
4571 *aVisibleRect = displayPort;
4572 if (aBuilder->IsReusingStackingContextItems() ||
4573 !aBuilder->IsPartialUpdate() || aBuilder->InInvalidSubtree() ||
4574 IsFrameModified()) {
4575 OverrideDirtyRect(displayPort);
4576 } else if (HasOverrideDirtyRegion()) {
4577 nsRect* rect = GetProperty(
4578 nsDisplayListBuilder::DisplayListBuildingDisplayPortRect());
4579 if (rect) {
4580 OverrideDirtyRect(*rect);
4583 } else if (mIsRoot) {
4584 // The displayPort getter takes care of adjusting for resolution. So if
4585 // we have resolution but no displayPort then we need to adjust for
4586 // resolution here.
4587 auto* presShell = PresShell();
4588 *aVisibleRect =
4589 aVisibleRect->RemoveResolution(presShell->GetResolution());
4590 *aDirtyRect = aDirtyRect->RemoveResolution(presShell->GetResolution());
4594 // Since making new layers is expensive, only create a scrollable layer
4595 // for some scroll frames.
4596 // When a displayport is being used, force building of a layer so that
4597 // the compositor can find the scrollable layer for async scrolling.
4598 // If the element is marked 'scrollgrab', also force building of a layer
4599 // so that APZ can implement scroll grabbing.
4600 mWillBuildScrollableLayer = hasDisplayPort ||
4601 nsContentUtils::HasScrollgrab(content) ||
4602 mZoomableByAPZ;
4603 return mWillBuildScrollableLayer;
4606 void nsHTMLScrollFrame::NotifyApzTransaction() {
4607 mAllowScrollOriginDowngrade = true;
4608 mApzScrollPos = GetScrollPosition();
4609 mApzAnimationRequested = IsLastScrollUpdateAnimating();
4610 mApzAnimationTriggeredByScriptRequested =
4611 IsLastScrollUpdateTriggeredByScriptAnimating();
4612 mScrollUpdates.Clear();
4613 if (mIsRoot) {
4614 PresShell()->SetResolutionUpdated(false);
4618 Maybe<ScrollMetadata> nsHTMLScrollFrame::ComputeScrollMetadata(
4619 WebRenderLayerManager* aLayerManager, const nsIFrame* aItemFrame,
4620 const nsPoint& aOffsetToReferenceFrame) const {
4621 if (!mWillBuildScrollableLayer) {
4622 return Nothing();
4625 bool isRootContent =
4626 mIsRoot && PresContext()->IsRootContentDocumentCrossProcess();
4628 MOZ_ASSERT(mScrolledFrame->GetContent());
4630 return Some(nsLayoutUtils::ComputeScrollMetadata(
4631 mScrolledFrame, this, GetContent(), aItemFrame, aOffsetToReferenceFrame,
4632 aLayerManager, mScrollParentID, mScrollPort.Size(), isRootContent));
4635 bool nsHTMLScrollFrame::IsRectNearlyVisible(const nsRect& aRect) const {
4636 // Use the right rect depending on if a display port is set.
4637 nsRect displayPort;
4638 bool usingDisplayport = DisplayPortUtils::GetDisplayPort(
4639 GetContent(), &displayPort,
4640 DisplayPortOptions().With(DisplayportRelativeTo::ScrollFrame));
4642 if (mIsRoot && !usingDisplayport &&
4643 PresContext()->IsRootContentDocumentInProcess() &&
4644 !PresContext()->IsRootContentDocumentCrossProcess()) {
4645 // In the case of the root scroller of OOP iframes, there are cases where
4646 // any display port value isn't set, e.g. the iframe element is out of view
4647 // in the parent document. In such cases we'd consider the iframe is not
4648 // visible.
4649 return false;
4652 return aRect.Intersects(
4653 ExpandRectToNearlyVisible(usingDisplayport ? displayPort : mScrollPort));
4656 OverscrollBehaviorInfo nsHTMLScrollFrame::GetOverscrollBehaviorInfo() const {
4657 nsIFrame* frame = GetFrameForStyle();
4658 if (!frame) {
4659 return {};
4662 auto& disp = *frame->StyleDisplay();
4663 return OverscrollBehaviorInfo::FromStyleConstants(disp.mOverscrollBehaviorX,
4664 disp.mOverscrollBehaviorY);
4667 ScrollStyles nsHTMLScrollFrame::GetScrollStyles() const {
4668 nsPresContext* presContext = PresContext();
4669 if (!presContext->IsDynamic() &&
4670 !(mIsRoot && presContext->HasPaginatedScrolling())) {
4671 return ScrollStyles(StyleOverflow::Hidden, StyleOverflow::Hidden);
4674 if (!mIsRoot) {
4675 return ScrollStyles(*StyleDisplay(),
4676 ScrollStyles::MapOverflowToValidScrollStyle);
4679 ScrollStyles result = presContext->GetViewportScrollStylesOverride();
4680 if (nsDocShell* ds = presContext->GetDocShell()) {
4681 switch (ds->ScrollbarPreference()) {
4682 case ScrollbarPreference::Auto:
4683 break;
4684 case ScrollbarPreference::Never:
4685 result.mHorizontal = result.mVertical = StyleOverflow::Hidden;
4686 break;
4689 return result;
4692 nsRect nsHTMLScrollFrame::GetLayoutScrollRange() const {
4693 return GetScrollRange(mScrollPort.width, mScrollPort.height);
4696 nsRect nsHTMLScrollFrame::GetScrollRange(nscoord aWidth,
4697 nscoord aHeight) const {
4698 nsRect range = GetScrolledRect();
4699 range.width = std::max(range.width - aWidth, 0);
4700 range.height = std::max(range.height - aHeight, 0);
4701 return range;
4704 nsRect nsHTMLScrollFrame::GetVisualScrollRange() const {
4705 nsSize visualViewportSize = GetVisualViewportSize();
4706 return GetScrollRange(visualViewportSize.width, visualViewportSize.height);
4709 nsSize nsHTMLScrollFrame::GetVisualViewportSize() const {
4710 auto* presShell = PresShell();
4711 if (mIsRoot && presShell->IsVisualViewportSizeSet()) {
4712 return presShell->GetVisualViewportSize();
4714 return mScrollPort.Size();
4717 nsPoint nsHTMLScrollFrame::GetVisualViewportOffset() const {
4718 if (mIsRoot) {
4719 auto* presShell = PresShell();
4720 if (auto pendingUpdate = presShell->GetPendingVisualScrollUpdate()) {
4721 // The pending visual scroll update on the PresShell contains a raw,
4722 // unclamped offset (basically, whatever was passed to ScrollToVisual()).
4723 // It will be clamped on the APZ side, but if we use it as the
4724 // main-thread's visual viewport offset we need to clamp it ourselves.
4725 // Use GetScrollRangeForUserInputEvents() to do the clamping because this
4726 // the scroll range that APZ will use.
4727 return GetScrollRangeForUserInputEvents().ClampPoint(
4728 pendingUpdate->mVisualScrollOffset);
4730 return presShell->GetVisualViewportOffset();
4732 return GetScrollPosition();
4735 bool nsHTMLScrollFrame::SetVisualViewportOffset(const nsPoint& aOffset,
4736 bool aRepaint) {
4737 MOZ_ASSERT(mIsRoot);
4738 AutoWeakFrame weakFrame(this);
4739 AutoScrollbarRepaintSuppression repaintSuppression(this, weakFrame,
4740 !aRepaint);
4742 bool retVal =
4743 PresShell()->SetVisualViewportOffset(aOffset, GetScrollPosition());
4744 if (!weakFrame.IsAlive()) {
4745 return false;
4747 return retVal;
4750 nsRect nsHTMLScrollFrame::GetVisualOptimalViewingRect() const {
4751 auto* presShell = PresShell();
4752 nsRect rect = mScrollPort;
4753 if (mIsRoot && presShell->IsVisualViewportSizeSet() &&
4754 presShell->IsVisualViewportOffsetSet()) {
4755 rect = nsRect(mScrollPort.TopLeft() - GetScrollPosition() +
4756 presShell->GetVisualViewportOffset(),
4757 presShell->GetVisualViewportSize());
4759 // NOTE: We intentionally resolve scroll-padding percentages against the
4760 // scrollport even when the visual viewport is set, see
4761 // https://github.com/w3c/csswg-drafts/issues/4393.
4762 rect.Deflate(GetScrollPadding());
4763 return rect;
4766 static void AdjustDestinationForWholeDelta(const nsIntPoint& aDelta,
4767 const nsRect& aScrollRange,
4768 nsPoint& aPoint) {
4769 if (aDelta.x < 0) {
4770 aPoint.x = aScrollRange.X();
4771 } else if (aDelta.x > 0) {
4772 aPoint.x = aScrollRange.XMost();
4774 if (aDelta.y < 0) {
4775 aPoint.y = aScrollRange.Y();
4776 } else if (aDelta.y > 0) {
4777 aPoint.y = aScrollRange.YMost();
4782 * Calculate lower/upper scrollBy range in given direction.
4783 * @param aDelta specifies scrollBy direction, if 0 then range will be 0 size
4784 * @param aPos desired destination in AppUnits
4785 * @param aNeg/PosTolerance defines relative range distance
4786 * below and above of aPos point
4787 * @param aMultiplier used for conversion of tolerance into appUnis
4789 static void CalcRangeForScrollBy(int32_t aDelta, nscoord aPos,
4790 float aNegTolerance, float aPosTolerance,
4791 nscoord aMultiplier, nscoord* aLower,
4792 nscoord* aUpper) {
4793 if (!aDelta) {
4794 *aLower = *aUpper = aPos;
4795 return;
4797 *aLower = aPos - NSToCoordRound(aMultiplier *
4798 (aDelta > 0 ? aNegTolerance : aPosTolerance));
4799 *aUpper = aPos + NSToCoordRound(aMultiplier *
4800 (aDelta > 0 ? aPosTolerance : aNegTolerance));
4803 void nsHTMLScrollFrame::ScrollBy(nsIntPoint aDelta, ScrollUnit aUnit,
4804 ScrollMode aMode, nsIntPoint* aOverflow,
4805 ScrollOrigin aOrigin,
4806 nsIScrollableFrame::ScrollMomentum aMomentum,
4807 ScrollSnapFlags aSnapFlags) {
4808 // When a smooth scroll is being processed on a frame, mouse wheel and
4809 // trackpad momentum scroll event updates must notcancel the SMOOTH or
4810 // SMOOTH_MSD scroll animations, enabling Javascript that depends on them to
4811 // be responsive without forcing the user to wait for the fling animations to
4812 // completely stop.
4813 switch (aMomentum) {
4814 case nsIScrollableFrame::NOT_MOMENTUM:
4815 mIgnoreMomentumScroll = false;
4816 break;
4817 case nsIScrollableFrame::SYNTHESIZED_MOMENTUM_EVENT:
4818 if (mIgnoreMomentumScroll) {
4819 return;
4821 break;
4824 if (mAsyncSmoothMSDScroll != nullptr) {
4825 // When CSSOM-View scroll-behavior smooth scrolling is interrupted,
4826 // the scroll is not completed to avoid non-smooth snapping to the
4827 // prior smooth scroll's destination.
4828 mDestination = GetScrollPosition();
4831 nsSize deltaMultiplier;
4832 float negativeTolerance;
4833 float positiveTolerance;
4834 if (aOrigin == ScrollOrigin::NotSpecified) {
4835 aOrigin = ScrollOrigin::Other;
4837 bool isGenericOrigin = (aOrigin == ScrollOrigin::Other);
4839 bool askApzToDoTheScroll = false;
4840 if ((aSnapFlags == ScrollSnapFlags::Disabled || !NeedsScrollSnap()) &&
4841 gfxPlatform::UseDesktopZoomingScrollbars() &&
4842 nsLayoutUtils::AsyncPanZoomEnabled(this) &&
4843 !nsLayoutUtils::ShouldDisableApzForElement(GetContent()) &&
4844 (WantAsyncScroll() || mZoomableByAPZ) &&
4845 CanApzScrollInTheseDirections(DirectionsInDelta(aDelta))) {
4846 askApzToDoTheScroll = true;
4849 switch (aUnit) {
4850 case ScrollUnit::DEVICE_PIXELS: {
4851 nscoord appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel();
4852 deltaMultiplier = nsSize(appUnitsPerDevPixel, appUnitsPerDevPixel);
4853 if (isGenericOrigin) {
4854 aOrigin = ScrollOrigin::Pixels;
4856 negativeTolerance = positiveTolerance = 0.5f;
4857 break;
4859 case ScrollUnit::LINES: {
4860 deltaMultiplier = GetLineScrollAmount();
4861 if (isGenericOrigin) {
4862 aOrigin = ScrollOrigin::Lines;
4864 negativeTolerance = positiveTolerance = 0.1f;
4865 break;
4867 case ScrollUnit::PAGES: {
4868 deltaMultiplier = GetPageScrollAmount();
4869 if (isGenericOrigin) {
4870 aOrigin = ScrollOrigin::Pages;
4872 negativeTolerance = 0.05f;
4873 positiveTolerance = 0;
4874 break;
4876 case ScrollUnit::WHOLE: {
4877 if (askApzToDoTheScroll) {
4878 MOZ_ASSERT(aDelta.x >= -1 && aDelta.x <= 1 && aDelta.y >= -1 &&
4879 aDelta.y <= 1);
4880 deltaMultiplier = GetScrollRangeForUserInputEvents().Size();
4881 break;
4882 } else {
4883 nsPoint pos = GetScrollPosition();
4884 AdjustDestinationForWholeDelta(aDelta, GetLayoutScrollRange(), pos);
4885 ScrollToWithOrigin(
4886 pos, nullptr /* range */,
4887 ScrollOperationParams{aMode, ScrollOrigin::Other, aSnapFlags,
4888 ScrollTriggeredByScript::No});
4889 // 'this' might be destroyed here
4890 if (aOverflow) {
4891 *aOverflow = nsIntPoint(0, 0);
4893 return;
4896 default:
4897 NS_ERROR("Invalid scroll mode");
4898 return;
4901 if (askApzToDoTheScroll) {
4902 nsPoint delta(
4903 NSCoordSaturatingNonnegativeMultiply(aDelta.x, deltaMultiplier.width),
4904 NSCoordSaturatingNonnegativeMultiply(aDelta.y, deltaMultiplier.height));
4906 AppendScrollUpdate(
4907 ScrollPositionUpdate::NewPureRelativeScroll(aOrigin, aMode, delta));
4909 nsIContent* content = GetContent();
4910 if (!DisplayPortUtils::HasNonMinimalNonZeroDisplayPort(content)) {
4911 if (MOZ_LOG_TEST(sDisplayportLog, LogLevel::Debug)) {
4912 mozilla::layers::ScrollableLayerGuid::ViewID viewID =
4913 mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID;
4914 nsLayoutUtils::FindIDFor(content, &viewID);
4915 MOZ_LOG(
4916 sDisplayportLog, LogLevel::Debug,
4917 ("ScrollBy setting displayport on scrollId=%" PRIu64 "\n", viewID));
4920 DisplayPortUtils::CalculateAndSetDisplayPortMargins(
4921 GetScrollTargetFrame(), DisplayPortUtils::RepaintMode::Repaint);
4922 nsIFrame* frame = do_QueryFrame(GetScrollTargetFrame());
4923 DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(
4924 frame);
4927 SchedulePaint();
4928 return;
4931 nsPoint newPos(NSCoordSaturatingAdd(mDestination.x,
4932 NSCoordSaturatingNonnegativeMultiply(
4933 aDelta.x, deltaMultiplier.width)),
4934 NSCoordSaturatingAdd(mDestination.y,
4935 NSCoordSaturatingNonnegativeMultiply(
4936 aDelta.y, deltaMultiplier.height)));
4938 Maybe<SnapDestination> snapDestination;
4939 if (aSnapFlags != ScrollSnapFlags::Disabled) {
4940 if (NeedsScrollSnap()) {
4941 nscoord appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel();
4942 deltaMultiplier = nsSize(appUnitsPerDevPixel, appUnitsPerDevPixel);
4943 negativeTolerance = 0.1f;
4944 positiveTolerance = 0;
4945 ScrollUnit snapUnit = aUnit;
4946 if (aOrigin == ScrollOrigin::MouseWheel) {
4947 // When using a clicky scroll wheel, snap point selection works the same
4948 // as keyboard up/down/left/right navigation, but with varying amounts
4949 // of scroll delta.
4950 snapUnit = ScrollUnit::LINES;
4952 snapDestination = GetSnapPointForDestination(snapUnit, aSnapFlags,
4953 mDestination, newPos);
4954 if (snapDestination) {
4955 newPos = snapDestination->mPosition;
4960 // Calculate desired range values.
4961 nscoord rangeLowerX, rangeUpperX, rangeLowerY, rangeUpperY;
4962 CalcRangeForScrollBy(aDelta.x, newPos.x, negativeTolerance, positiveTolerance,
4963 deltaMultiplier.width, &rangeLowerX, &rangeUpperX);
4964 CalcRangeForScrollBy(aDelta.y, newPos.y, negativeTolerance, positiveTolerance,
4965 deltaMultiplier.height, &rangeLowerY, &rangeUpperY);
4966 nsRect range(rangeLowerX, rangeLowerY, rangeUpperX - rangeLowerX,
4967 rangeUpperY - rangeLowerY);
4968 AutoWeakFrame weakFrame(this);
4969 ScrollToWithOrigin(
4970 newPos, &range,
4971 snapDestination
4972 ? ScrollOperationParams{aMode, aOrigin,
4973 std::move(snapDestination->mTargetIds)}
4974 : ScrollOperationParams{aMode, aOrigin});
4975 if (!weakFrame.IsAlive()) {
4976 return;
4979 if (aOverflow) {
4980 nsPoint clampAmount = newPos - mDestination;
4981 float appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel();
4982 *aOverflow =
4983 nsIntPoint(NSAppUnitsToIntPixels(clampAmount.x, appUnitsPerDevPixel),
4984 NSAppUnitsToIntPixels(clampAmount.y, appUnitsPerDevPixel));
4987 if (aUnit == ScrollUnit::DEVICE_PIXELS &&
4988 !nsLayoutUtils::AsyncPanZoomEnabled(this)) {
4989 // When APZ is disabled, we must track the velocity
4990 // on the main thread; otherwise, the APZC will manage this.
4991 mVelocityQueue.Sample(GetScrollPosition());
4995 void nsHTMLScrollFrame::ScrollByCSSPixelsInternal(const CSSIntPoint& aDelta,
4996 ScrollMode aMode,
4997 ScrollSnapFlags aSnapFlags) {
4998 nsPoint current = GetScrollPosition();
4999 // `current` value above might be a value which was aligned to in
5000 // layer-pixels, so starting from such points will make the difference between
5001 // the given position in script (e.g. scrollTo) and the aligned position
5002 // larger, in the worst case the difference can be observed in CSS pixels.
5003 // To avoid it, we use the current position in CSS pixels as the start
5004 // position. Hopefully it exactly matches the position where it was given by
5005 // the previous scrolling operation, but there may be some edge cases where
5006 // the current position in CSS pixels differs from the given position, the
5007 // cases should be fixed in bug 1556685.
5008 CSSIntPoint currentCSSPixels = GetScrollPositionCSSPixels();
5009 nsPoint pt = CSSPoint::ToAppUnits(currentCSSPixels + aDelta);
5011 nscoord halfPixel = nsPresContext::CSSPixelsToAppUnits(0.5f);
5012 nsRect range(pt.x - halfPixel, pt.y - halfPixel, 2 * halfPixel - 1,
5013 2 * halfPixel - 1);
5014 // XXX I don't think the following blocks are needed anymore, now that
5015 // ScrollToImpl simply tries to scroll an integer number of layer
5016 // pixels from the current position
5017 if (aDelta.x == 0.0f) {
5018 pt.x = current.x;
5019 range.x = pt.x;
5020 range.width = 0;
5022 if (aDelta.y == 0.0f) {
5023 pt.y = current.y;
5024 range.y = pt.y;
5025 range.height = 0;
5027 ScrollToWithOrigin(
5028 pt, &range,
5029 ScrollOperationParams{aMode, ScrollOrigin::Relative, aSnapFlags,
5030 ScrollTriggeredByScript::Yes});
5031 // 'this' might be destroyed here
5034 void nsHTMLScrollFrame::ScrollSnap(ScrollMode aMode) {
5035 float flingSensitivity =
5036 StaticPrefs::layout_css_scroll_snap_prediction_sensitivity();
5037 int maxVelocity =
5038 StaticPrefs::layout_css_scroll_snap_prediction_max_velocity();
5039 maxVelocity = nsPresContext::CSSPixelsToAppUnits(maxVelocity);
5040 int maxOffset = maxVelocity * flingSensitivity;
5041 nsPoint velocity = mVelocityQueue.GetVelocity();
5042 // Multiply each component individually to avoid integer multiply
5043 nsPoint predictedOffset =
5044 nsPoint(velocity.x * flingSensitivity, velocity.y * flingSensitivity);
5045 predictedOffset.Clamp(maxOffset);
5046 nsPoint pos = GetScrollPosition();
5047 nsPoint destinationPos = pos + predictedOffset;
5048 ScrollSnap(destinationPos, aMode);
5051 void nsHTMLScrollFrame::ScrollSnap(const nsPoint& aDestination,
5052 ScrollMode aMode) {
5053 nsRect scrollRange = GetLayoutScrollRange();
5054 nsPoint pos = GetScrollPosition();
5055 nsPoint destination = scrollRange.ClampPoint(aDestination);
5056 ScrollSnapFlags snapFlags = ScrollSnapFlags::IntendedEndPosition;
5057 if (mVelocityQueue.GetVelocity() != nsPoint()) {
5058 snapFlags |= ScrollSnapFlags::IntendedDirection;
5061 // Bug 1776624 : Consider using mDestination as |aStartPos| argument for this
5062 // GetSnapPointForDestination call, this function call is the only one call
5063 // site using `GetScrollPosition()` as |aStartPos|.
5064 if (auto snapDestination = GetSnapPointForDestination(
5065 ScrollUnit::DEVICE_PIXELS, snapFlags, pos, destination)) {
5066 destination = snapDestination->mPosition;
5067 ScrollToWithOrigin(
5068 destination, nullptr /* range */,
5069 ScrollOperationParams{aMode, ScrollOrigin::Other,
5070 std::move(snapDestination->mTargetIds)});
5074 nsSize nsHTMLScrollFrame::GetLineScrollAmount() const {
5075 RefPtr<nsFontMetrics> fm =
5076 nsLayoutUtils::GetInflatedFontMetricsForFrame(this);
5077 NS_ASSERTION(fm, "FontMetrics is null, assuming fontHeight == 1 appunit");
5078 int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel();
5079 nscoord minScrollAmountInAppUnits =
5080 std::max(1, StaticPrefs::mousewheel_min_line_scroll_amount()) *
5081 appUnitsPerDevPixel;
5082 nscoord horizontalAmount = fm ? fm->AveCharWidth() : 0;
5083 nscoord verticalAmount = fm ? fm->MaxHeight() : 0;
5084 return nsSize(std::max(horizontalAmount, minScrollAmountInAppUnits),
5085 std::max(verticalAmount, minScrollAmountInAppUnits));
5089 * Compute the scrollport size excluding any fixed-pos and sticky-pos (that are
5090 * stuck) headers and footers. A header or footer is an box that spans that
5091 * entire width of the viewport and touches the top (or bottom, respectively) of
5092 * the viewport. We also want to consider fixed/sticky elements that stack or
5093 * overlap to effectively create a larger header or footer. Headers and footers
5094 * that cover more than a third of the the viewport are ignored since they
5095 * probably aren't true headers and footers and we don't want to restrict
5096 * scrolling too much in such cases. This is a bit conservative --- some
5097 * pages use elements as headers or footers that don't span the entire width
5098 * of the viewport --- but it should be a good start.
5100 * If aViewportFrame is non-null then the scroll frame is the root scroll
5101 * frame and we should consider fixed-pos items.
5103 struct TopAndBottom {
5104 TopAndBottom(nscoord aTop, nscoord aBottom) : top(aTop), bottom(aBottom) {}
5106 nscoord top, bottom;
5108 struct TopComparator {
5109 bool Equals(const TopAndBottom& A, const TopAndBottom& B) const {
5110 return A.top == B.top;
5112 bool LessThan(const TopAndBottom& A, const TopAndBottom& B) const {
5113 return A.top < B.top;
5116 struct ReverseBottomComparator {
5117 bool Equals(const TopAndBottom& A, const TopAndBottom& B) const {
5118 return A.bottom == B.bottom;
5120 bool LessThan(const TopAndBottom& A, const TopAndBottom& B) const {
5121 return A.bottom > B.bottom;
5125 static void AddToListIfHeaderFooter(nsIFrame* aFrame,
5126 nsIFrame* aScrollPortFrame,
5127 const nsRect& aScrollPort,
5128 nsTArray<TopAndBottom>& aList) {
5129 nsRect r = aFrame->GetRectRelativeToSelf();
5130 r = nsLayoutUtils::TransformFrameRectToAncestor(aFrame, r, aScrollPortFrame);
5131 r = r.Intersect(aScrollPort);
5132 if ((r.width >= aScrollPort.width / 2 ||
5133 r.width >= NSIntPixelsToAppUnits(800, AppUnitsPerCSSPixel())) &&
5134 r.height <= aScrollPort.height / 3) {
5135 aList.AppendElement(TopAndBottom(r.y, r.YMost()));
5139 static nsSize GetScrollPortSizeExcludingHeadersAndFooters(
5140 nsIFrame* aScrollFrame, nsIFrame* aViewportFrame,
5141 const nsRect& aScrollPort) {
5142 AutoTArray<TopAndBottom, 10> list;
5143 if (aViewportFrame) {
5144 for (nsIFrame* f : aViewportFrame->GetChildList(FrameChildListID::Fixed)) {
5145 AddToListIfHeaderFooter(f, aViewportFrame, aScrollPort, list);
5149 // Add sticky frames that are currently in "fixed" positions
5150 StickyScrollContainer* ssc =
5151 StickyScrollContainer::GetStickyScrollContainerForScrollFrame(
5152 aScrollFrame);
5153 if (ssc) {
5154 const nsTArray<nsIFrame*>& stickyFrames = ssc->GetFrames();
5155 for (nsIFrame* f : stickyFrames) {
5156 // If it's acting like fixed position.
5157 if (ssc->IsStuckInYDirection(f)) {
5158 AddToListIfHeaderFooter(f, aScrollFrame, aScrollPort, list);
5163 list.Sort(TopComparator());
5164 nscoord headerBottom = 0;
5165 for (uint32_t i = 0; i < list.Length(); ++i) {
5166 if (list[i].top <= headerBottom) {
5167 headerBottom = std::max(headerBottom, list[i].bottom);
5171 list.Sort(ReverseBottomComparator());
5172 nscoord footerTop = aScrollPort.height;
5173 for (uint32_t i = 0; i < list.Length(); ++i) {
5174 if (list[i].bottom >= footerTop) {
5175 footerTop = std::min(footerTop, list[i].top);
5179 headerBottom = std::min(aScrollPort.height / 3, headerBottom);
5180 footerTop = std::max(aScrollPort.height - aScrollPort.height / 3, footerTop);
5182 return nsSize(aScrollPort.width, footerTop - headerBottom);
5185 nsSize nsHTMLScrollFrame::GetPageScrollAmount() const {
5186 nsSize effectiveScrollPortSize;
5188 if (GetVisualViewportSize() != mScrollPort.Size()) {
5189 // We want to use the visual viewport size if one is set.
5190 // The headers/footers adjustment is too complicated to do if there is a
5191 // visual viewport that differs from the layout viewport, this is probably
5192 // okay.
5193 effectiveScrollPortSize = GetVisualViewportSize();
5194 } else {
5195 // Reduce effective scrollport height by the height of any
5196 // fixed-pos/sticky-pos headers or footers
5197 effectiveScrollPortSize = GetScrollPortSizeExcludingHeadersAndFooters(
5198 const_cast<nsHTMLScrollFrame*>(this),
5199 mIsRoot ? PresShell()->GetRootFrame() : nullptr, mScrollPort);
5202 nsSize lineScrollAmount = GetLineScrollAmount();
5204 // The page increment is the size of the page, minus the smaller of
5205 // 10% of the size or 2 lines.
5206 return nsSize(effectiveScrollPortSize.width -
5207 std::min(effectiveScrollPortSize.width / 10,
5208 2 * lineScrollAmount.width),
5209 effectiveScrollPortSize.height -
5210 std::min(effectiveScrollPortSize.height / 10,
5211 2 * lineScrollAmount.height));
5215 * this code is resposible for restoring the scroll position back to some
5216 * saved position. if the user has not moved the scroll position manually
5217 * we keep scrolling down until we get to our original position. keep in
5218 * mind that content could incrementally be coming in. we only want to stop
5219 * when we reach our new position.
5221 void nsHTMLScrollFrame::ScrollToRestoredPosition() {
5222 if (mRestorePos.y == -1 || mLastPos.x == -1 || mLastPos.y == -1) {
5223 return;
5225 // make sure our scroll position did not change for where we last put
5226 // it. if it does then the user must have moved it, and we no longer
5227 // need to restore.
5229 // In the RTL case, we check whether the scroll position changed using the
5230 // logical scroll position, but we scroll to the physical scroll position in
5231 // all cases
5233 // The layout offset we want to restore is the same as the visual offset
5234 // (for now, may change in bug 1499210), but clamped to the layout scroll
5235 // range (which can be a subset of the visual scroll range).
5236 // Note that we can't do the clamping when initializing mRestorePos in
5237 // RestoreState(), since the scrollable rect (which the clamping depends
5238 // on) can change over the course of the restoration process.
5239 nsPoint layoutRestorePos = GetLayoutScrollRange().ClampPoint(mRestorePos);
5240 nsPoint visualRestorePos = GetVisualScrollRange().ClampPoint(mRestorePos);
5242 // Continue restoring until both the layout and visual scroll positions
5243 // reach the destination. (Note that the two can only be different for
5244 // the root content document's root scroll frame, and when zoomed in).
5245 // This is necessary to avoid situations where the two offsets get stuck
5246 // at different values and nothing reconciles them (see bug 1519621 comment
5247 // 8).
5248 nsPoint logicalLayoutScrollPos = GetLogicalScrollPosition();
5250 SCROLLRESTORE_LOG(
5251 "%p: ScrollToRestoredPosition (mRestorePos=%s, mLastPos=%s, "
5252 "layoutRestorePos=%s, visualRestorePos=%s, "
5253 "logicalLayoutScrollPos=%s, "
5254 "GetLogicalVisualViewportOffset()=%s)\n",
5255 this, ToString(mRestorePos).c_str(), ToString(mLastPos).c_str(),
5256 ToString(layoutRestorePos).c_str(), ToString(visualRestorePos).c_str(),
5257 ToString(logicalLayoutScrollPos).c_str(),
5258 ToString(GetLogicalVisualViewportOffset()).c_str());
5260 // if we didn't move, we still need to restore
5261 if (GetLogicalVisualViewportOffset() == mLastPos ||
5262 logicalLayoutScrollPos == mLastPos) {
5263 // if our desired position is different to the scroll position, scroll.
5264 // remember that we could be incrementally loading so we may enter
5265 // and scroll many times.
5266 if (mRestorePos != mLastPos /* GetLogicalVisualViewportOffset() */ ||
5267 layoutRestorePos != logicalLayoutScrollPos) {
5268 LoadingState state = GetPageLoadingState();
5269 if (state == LoadingState::Stopped && !IsSubtreeDirty()) {
5270 return;
5272 nsPoint visualScrollToPos = visualRestorePos;
5273 nsPoint layoutScrollToPos = layoutRestorePos;
5274 if (!IsPhysicalLTR()) {
5275 // convert from logical to physical scroll position
5276 visualScrollToPos.x -=
5277 (GetVisualViewportSize().width - mScrolledFrame->GetRect().width);
5278 layoutScrollToPos.x -=
5279 (GetVisualViewportSize().width - mScrolledFrame->GetRect().width);
5281 AutoWeakFrame weakFrame(this);
5282 // It's very important to pass ScrollOrigin::Restore here, so
5283 // ScrollToWithOrigin won't clear out mRestorePos.
5284 ScrollToWithOrigin(
5285 layoutScrollToPos, nullptr,
5286 ScrollOperationParams{ScrollMode::Instant, ScrollOrigin::Restore});
5287 if (!weakFrame.IsAlive()) {
5288 return;
5290 if (mIsRoot) {
5291 PresShell()->ScrollToVisual(visualScrollToPos, FrameMetrics::eRestore,
5292 ScrollMode::Instant);
5294 if (state == LoadingState::Loading || IsSubtreeDirty()) {
5295 // If we're trying to do a history scroll restore, then we want to
5296 // keep trying this until we succeed, because the page can be loading
5297 // incrementally. So re-get the scroll position for the next iteration,
5298 // it might not be exactly equal to mRestorePos due to rounding and
5299 // clamping.
5300 mLastPos = GetLogicalVisualViewportOffset();
5301 return;
5304 // If we get here, either we reached the desired position (mLastPos ==
5305 // mRestorePos) or we're not trying to do a history scroll restore, so
5306 // we can stop after the scroll attempt above.
5307 mRestorePos.y = -1;
5308 mLastPos.x = -1;
5309 mLastPos.y = -1;
5310 } else {
5311 // user moved the position, so we won't need to restore
5312 mLastPos.x = -1;
5313 mLastPos.y = -1;
5317 auto nsHTMLScrollFrame::GetPageLoadingState() -> LoadingState {
5318 bool loadCompleted = false, stopped = false;
5319 nsCOMPtr<nsIDocShell> ds = GetContent()->GetComposedDoc()->GetDocShell();
5320 if (ds) {
5321 nsCOMPtr<nsIDocumentViewer> viewer;
5322 ds->GetDocViewer(getter_AddRefs(viewer));
5323 if (viewer) {
5324 loadCompleted = viewer->GetLoadCompleted();
5325 stopped = viewer->GetIsStopped();
5328 return loadCompleted
5329 ? (stopped ? LoadingState::Stopped : LoadingState::Loaded)
5330 : LoadingState::Loading;
5333 nsHTMLScrollFrame::OverflowState nsHTMLScrollFrame::GetOverflowState() const {
5334 nsSize scrollportSize = mScrollPort.Size();
5335 nsSize childSize = GetScrolledRect().Size();
5337 OverflowState result = OverflowState::None;
5339 if (childSize.height > scrollportSize.height) {
5340 result |= OverflowState::Vertical;
5343 if (childSize.width > scrollportSize.width) {
5344 result |= OverflowState::Horizontal;
5347 return result;
5350 nsresult nsHTMLScrollFrame::FireScrollPortEvent() {
5351 mAsyncScrollPortEvent.Forget();
5353 // TODO(emilio): why do we need the whole WillPaintObserver infrastructure and
5354 // can't use AddScriptRunner & co? I guess it made sense when we used
5355 // WillPaintObserver for scroll events too, or when this used to flush.
5357 // Should we remove this?
5359 OverflowState overflowState = GetOverflowState();
5361 bool newVerticalOverflow = !!(overflowState & OverflowState::Vertical);
5362 bool vertChanged = mVerticalOverflow != newVerticalOverflow;
5364 bool newHorizontalOverflow = !!(overflowState & OverflowState::Horizontal);
5365 bool horizChanged = mHorizontalOverflow != newHorizontalOverflow;
5367 if (!vertChanged && !horizChanged) {
5368 return NS_OK;
5371 // If both either overflowed or underflowed then we dispatch only one
5372 // DOM event.
5373 bool both = vertChanged && horizChanged &&
5374 newVerticalOverflow == newHorizontalOverflow;
5375 InternalScrollPortEvent::OrientType orient;
5376 if (both) {
5377 orient = InternalScrollPortEvent::eBoth;
5378 mHorizontalOverflow = newHorizontalOverflow;
5379 mVerticalOverflow = newVerticalOverflow;
5380 } else if (vertChanged) {
5381 orient = InternalScrollPortEvent::eVertical;
5382 mVerticalOverflow = newVerticalOverflow;
5383 if (horizChanged) {
5384 // We need to dispatch a separate horizontal DOM event. Do that the next
5385 // time around since dispatching the vertical DOM event might destroy
5386 // the frame.
5387 PostOverflowEvent();
5389 } else {
5390 orient = InternalScrollPortEvent::eHorizontal;
5391 mHorizontalOverflow = newHorizontalOverflow;
5394 InternalScrollPortEvent event(
5395 true,
5396 (orient == InternalScrollPortEvent::eHorizontal ? mHorizontalOverflow
5397 : mVerticalOverflow)
5398 ? eScrollPortOverflow
5399 : eScrollPortUnderflow,
5400 nullptr);
5401 event.mOrient = orient;
5403 RefPtr<nsIContent> content = GetContent();
5404 RefPtr<nsPresContext> presContext = PresContext();
5405 return EventDispatcher::Dispatch(content, presContext, &event);
5408 void nsHTMLScrollFrame::PostScrollEndEvent() {
5409 if (mScrollEndEvent) {
5410 return;
5413 // The ScrollEndEvent constructor registers itself with the refresh driver.
5414 mScrollEndEvent = new ScrollEndEvent(this);
5417 void nsHTMLScrollFrame::FireScrollEndEvent() {
5418 MOZ_ASSERT(GetContent());
5419 MOZ_ASSERT(mScrollEndEvent);
5421 RefPtr<nsPresContext> presContext = PresContext();
5422 mScrollEndEvent->Revoke();
5423 mScrollEndEvent = nullptr;
5425 nsEventStatus status = nsEventStatus_eIgnore;
5426 WidgetGUIEvent event(true, eScrollend, nullptr);
5427 event.mFlags.mBubbles = mIsRoot;
5428 event.mFlags.mCancelable = false;
5429 // If apz.scrollend-event.content.enabled is not set, the event should
5430 // only be dispatched to the browser chrome.
5431 event.mFlags.mOnlyChromeDispatch =
5432 !StaticPrefs::apz_scrollend_event_content_enabled();
5433 RefPtr<nsINode> target =
5434 mIsRoot ? static_cast<nsINode*>(presContext->Document()) : GetContent();
5435 EventDispatcher::Dispatch(target, presContext, &event, nullptr, &status);
5438 void nsHTMLScrollFrame::ReloadChildFrames() {
5439 mScrolledFrame = nullptr;
5440 mHScrollbarBox = nullptr;
5441 mVScrollbarBox = nullptr;
5442 mScrollCornerBox = nullptr;
5443 mResizerBox = nullptr;
5445 for (nsIFrame* frame : PrincipalChildList()) {
5446 nsIContent* content = frame->GetContent();
5447 if (content == GetContent()) {
5448 NS_ASSERTION(!mScrolledFrame, "Already found the scrolled frame");
5449 mScrolledFrame = frame;
5450 } else {
5451 nsAutoString value;
5452 if (content->IsElement()) {
5453 content->AsElement()->GetAttr(nsGkAtoms::orient, value);
5455 if (!value.IsEmpty()) {
5456 // probably a scrollbar then
5457 if (value.LowerCaseEqualsLiteral("horizontal")) {
5458 NS_ASSERTION(!mHScrollbarBox,
5459 "Found multiple horizontal scrollbars?");
5460 mHScrollbarBox = do_QueryFrame(frame);
5461 MOZ_ASSERT(mHScrollbarBox, "Not a scrollbar?");
5462 } else {
5463 NS_ASSERTION(!mVScrollbarBox, "Found multiple vertical scrollbars?");
5464 mVScrollbarBox = do_QueryFrame(frame);
5465 MOZ_ASSERT(mVScrollbarBox, "Not a scrollbar?");
5467 } else if (content->IsXULElement(nsGkAtoms::resizer)) {
5468 NS_ASSERTION(!mResizerBox, "Found multiple resizers");
5469 mResizerBox = frame;
5470 } else if (content->IsXULElement(nsGkAtoms::scrollcorner)) {
5471 // probably a scrollcorner
5472 NS_ASSERTION(!mScrollCornerBox, "Found multiple scrollcorners");
5473 mScrollCornerBox = frame;
5479 already_AddRefed<Element> nsHTMLScrollFrame::MakeScrollbar(
5480 NodeInfo* aNodeInfo, bool aVertical, AnonymousContentKey& aKey) {
5481 MOZ_ASSERT(aNodeInfo);
5482 MOZ_ASSERT(
5483 aNodeInfo->Equals(nsGkAtoms::scrollbar, nullptr, kNameSpaceID_XUL));
5485 static constexpr nsLiteralString kOrientValues[2] = {
5486 u"horizontal"_ns,
5487 u"vertical"_ns,
5490 aKey = AnonymousContentKey::Type_Scrollbar;
5491 if (aVertical) {
5492 aKey |= AnonymousContentKey::Flag_Vertical;
5495 RefPtr<Element> e;
5496 NS_TrustedNewXULElement(getter_AddRefs(e), do_AddRef(aNodeInfo));
5498 #ifdef DEBUG
5499 // Scrollbars can get restyled by theme changes. Whether such a restyle
5500 // will actually reconstruct them correctly if it involves a frame
5501 // reconstruct... I don't know. :(
5502 e->SetProperty(nsGkAtoms::restylableAnonymousNode,
5503 reinterpret_cast<void*>(true));
5504 #endif // DEBUG
5506 e->SetAttr(kNameSpaceID_None, nsGkAtoms::orient, kOrientValues[aVertical],
5507 false);
5509 if (mIsRoot) {
5510 e->SetProperty(nsGkAtoms::docLevelNativeAnonymousContent,
5511 reinterpret_cast<void*>(true));
5512 e->SetAttr(kNameSpaceID_None, nsGkAtoms::root_, u"true"_ns, false);
5514 // Don't bother making style caching take [root="true"] styles into account.
5515 aKey = AnonymousContentKey::None;
5518 return e.forget();
5521 bool nsHTMLScrollFrame::IsForTextControlWithNoScrollbars() const {
5522 // FIXME(emilio): we should probably make the scroller inside <input> an
5523 // internal pseudo-element, and then this would be simpler.
5525 // Also, this could just use scrollbar-width these days.
5526 auto* content = GetContent();
5527 if (!content) {
5528 return false;
5530 auto* input = content->GetClosestNativeAnonymousSubtreeRootParentOrHost();
5531 return input && input->IsHTMLElement(nsGkAtoms::input);
5534 auto nsHTMLScrollFrame::GetCurrentAnonymousContent() const
5535 -> EnumSet<AnonymousContentType> {
5536 EnumSet<AnonymousContentType> result;
5537 if (mHScrollbarContent) {
5538 result += AnonymousContentType::HorizontalScrollbar;
5540 if (mVScrollbarContent) {
5541 result += AnonymousContentType::VerticalScrollbar;
5543 if (mResizerContent) {
5544 result += AnonymousContentType::Resizer;
5546 return result;
5549 auto nsHTMLScrollFrame::GetNeededAnonymousContent() const
5550 -> EnumSet<AnonymousContentType> {
5551 nsPresContext* pc = PresContext();
5553 // Don't create scrollbars if we're an SVG document being used as an image,
5554 // or if we're printing/print previewing.
5555 // (In the printing case, we allow scrollbars if this is the child of the
5556 // viewport & paginated scrolling is enabled, because then we must be the
5557 // scroll frame for the print preview window, & that does need scrollbars.)
5558 if (pc->Document()->IsBeingUsedAsImage() ||
5559 (!pc->IsDynamic() && !(mIsRoot && pc->HasPaginatedScrolling()))) {
5560 return {};
5563 if (IsForTextControlWithNoScrollbars()) {
5564 return {};
5567 EnumSet<AnonymousContentType> result;
5568 // If we're the scrollframe for the root, then we want to construct our
5569 // scrollbar frames no matter what. That way later dynamic changes to
5570 // propagated overflow styles will show or hide scrollbars on the viewport
5571 // without requiring frame reconstruction of the viewport (good!).
5573 // TODO(emilio): Figure out if we can remove this special-case now that we
5574 // have more targeted optimizations.
5575 if (mIsRoot) {
5576 result += AnonymousContentType::HorizontalScrollbar;
5577 result += AnonymousContentType::VerticalScrollbar;
5578 // If scrollbar-width is none, don't generate scrollbars.
5579 } else if (StyleUIReset()->ScrollbarWidth() != StyleScrollbarWidth::None) {
5580 ScrollStyles styles = GetScrollStyles();
5581 if (styles.mHorizontal != StyleOverflow::Hidden) {
5582 result += AnonymousContentType::HorizontalScrollbar;
5584 if (styles.mVertical != StyleOverflow::Hidden) {
5585 result += AnonymousContentType::VerticalScrollbar;
5588 // If we have scrollbar-gutter, construct the scrollbar frames to query its
5589 // size to reserve the gutter space at the inline start or end edges.
5590 if (StyleDisplay()->mScrollbarGutter & StyleScrollbarGutter::STABLE) {
5591 result += GetWritingMode().IsVertical()
5592 ? AnonymousContentType::HorizontalScrollbar
5593 : AnonymousContentType::VerticalScrollbar;
5597 // Check if the frame is resizable. Note:
5598 // "The effect of the resize property on generated content is undefined.
5599 // Implementations should not apply the resize property to generated
5600 // content." [1]
5601 // For info on what is generated content, see [2].
5602 // [1]: https://drafts.csswg.org/css-ui/#resize
5603 // [2]: https://www.w3.org/TR/CSS2/generate.html#content
5604 auto resizeStyle = StyleDisplay()->mResize;
5605 if (resizeStyle != StyleResize::None &&
5606 !HasAnyStateBits(NS_FRAME_GENERATED_CONTENT)) {
5607 result += AnonymousContentType::Resizer;
5610 return result;
5613 nsresult nsHTMLScrollFrame::CreateAnonymousContent(
5614 nsTArray<nsIAnonymousContentCreator::ContentInfo>& aElements) {
5615 typedef nsIAnonymousContentCreator::ContentInfo ContentInfo;
5617 nsPresContext* presContext = PresContext();
5618 nsNodeInfoManager* nodeInfoManager =
5619 presContext->Document()->NodeInfoManager();
5621 auto neededAnonContent = GetNeededAnonymousContent();
5622 if (neededAnonContent.isEmpty()) {
5623 return NS_OK;
5627 RefPtr<NodeInfo> nodeInfo = nodeInfoManager->GetNodeInfo(
5628 nsGkAtoms::scrollbar, nullptr, kNameSpaceID_XUL, nsINode::ELEMENT_NODE);
5629 NS_ENSURE_TRUE(nodeInfo, NS_ERROR_OUT_OF_MEMORY);
5631 if (neededAnonContent.contains(AnonymousContentType::HorizontalScrollbar)) {
5632 AnonymousContentKey key;
5633 mHScrollbarContent = MakeScrollbar(nodeInfo, /* aVertical */ false, key);
5634 aElements.AppendElement(ContentInfo(mHScrollbarContent, key));
5637 if (neededAnonContent.contains(AnonymousContentType::VerticalScrollbar)) {
5638 AnonymousContentKey key;
5639 mVScrollbarContent = MakeScrollbar(nodeInfo, /* aVertical */ true, key);
5640 aElements.AppendElement(ContentInfo(mVScrollbarContent, key));
5644 if (neededAnonContent.contains(AnonymousContentType::Resizer)) {
5645 MOZ_ASSERT(!mIsRoot, "Root scroll frame shouldn't be resizable");
5647 RefPtr<NodeInfo> nodeInfo;
5648 nodeInfo = nodeInfoManager->GetNodeInfo(
5649 nsGkAtoms::resizer, nullptr, kNameSpaceID_XUL, nsINode::ELEMENT_NODE);
5650 NS_ENSURE_TRUE(nodeInfo, NS_ERROR_OUT_OF_MEMORY);
5652 NS_TrustedNewXULElement(getter_AddRefs(mResizerContent), nodeInfo.forget());
5654 nsAutoString dir;
5655 switch (StyleDisplay()->mResize) {
5656 case StyleResize::Horizontal:
5657 if (IsScrollbarOnRight()) {
5658 dir.AssignLiteral("right");
5659 } else {
5660 dir.AssignLiteral("left");
5662 break;
5663 case StyleResize::Vertical:
5664 dir.AssignLiteral("bottom");
5665 if (!IsScrollbarOnRight()) {
5666 mResizerContent->SetAttr(kNameSpaceID_None, nsGkAtoms::flip, u""_ns,
5667 false);
5669 break;
5670 case StyleResize::Both:
5671 if (IsScrollbarOnRight()) {
5672 dir.AssignLiteral("bottomright");
5673 } else {
5674 dir.AssignLiteral("bottomleft");
5676 break;
5677 default:
5678 NS_WARNING("only resizable types should have resizers");
5680 mResizerContent->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, dir, false);
5681 aElements.AppendElement(mResizerContent);
5684 if (neededAnonContent.contains(AnonymousContentType::HorizontalScrollbar) &&
5685 neededAnonContent.contains(AnonymousContentType::VerticalScrollbar)) {
5686 AnonymousContentKey key = AnonymousContentKey::Type_ScrollCorner;
5688 RefPtr<NodeInfo> nodeInfo =
5689 nodeInfoManager->GetNodeInfo(nsGkAtoms::scrollcorner, nullptr,
5690 kNameSpaceID_XUL, nsINode::ELEMENT_NODE);
5691 NS_TrustedNewXULElement(getter_AddRefs(mScrollCornerContent),
5692 nodeInfo.forget());
5693 if (mIsRoot) {
5694 mScrollCornerContent->SetProperty(
5695 nsGkAtoms::docLevelNativeAnonymousContent,
5696 reinterpret_cast<void*>(true));
5697 mScrollCornerContent->SetAttr(kNameSpaceID_None, nsGkAtoms::root_,
5698 u"true"_ns, false);
5700 // Don't bother making style caching take [root="true"] styles into
5701 // account.
5702 key = AnonymousContentKey::None;
5704 aElements.AppendElement(ContentInfo(mScrollCornerContent, key));
5707 // Don't cache styles if we are a child of a <select> element, since we have
5708 // some UA style sheet rules that depend on the <select>'s attributes.
5709 if (GetContent()->IsHTMLElement(nsGkAtoms::select)) {
5710 for (auto& info : aElements) {
5711 info.mKey = AnonymousContentKey::None;
5715 return NS_OK;
5718 void nsHTMLScrollFrame::AppendAnonymousContentTo(
5719 nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
5720 if (mHScrollbarContent) {
5721 aElements.AppendElement(mHScrollbarContent);
5724 if (mVScrollbarContent) {
5725 aElements.AppendElement(mVScrollbarContent);
5728 if (mScrollCornerContent) {
5729 aElements.AppendElement(mScrollCornerContent);
5732 if (mResizerContent) {
5733 aElements.AppendElement(mResizerContent);
5737 void nsHTMLScrollFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) {
5738 nsContainerFrame::DidSetComputedStyle(aOldComputedStyle);
5739 if (aOldComputedStyle && !mIsRoot &&
5740 StyleDisplay()->mScrollSnapType !=
5741 aOldComputedStyle->StyleDisplay()->mScrollSnapType) {
5742 PostPendingResnap();
5746 void nsHTMLScrollFrame::RemoveObservers() {
5747 if (mAsyncScroll) {
5748 mAsyncScroll->RemoveObserver();
5749 mAsyncScroll = nullptr;
5751 if (mAsyncSmoothMSDScroll) {
5752 mAsyncSmoothMSDScroll->RemoveObserver();
5753 mAsyncSmoothMSDScroll = nullptr;
5758 * Called when we want to update the scrollbar position, either because
5759 * scrolling happened or the user moved the scrollbar position and we need to
5760 * undo that (e.g., when the user clicks to scroll and we're using smooth
5761 * scrolling, so we need to put the thumb back to its initial position for the
5762 * start of the smooth sequence).
5764 void nsHTMLScrollFrame::UpdateScrollbarPosition() {
5765 AutoWeakFrame weakFrame(this);
5766 mFrameIsUpdatingScrollbar = true;
5768 nsPoint pt = GetScrollPosition();
5769 nsRect scrollRange = GetVisualScrollRange();
5771 if (gfxPlatform::UseDesktopZoomingScrollbars()) {
5772 pt = GetVisualViewportOffset();
5773 scrollRange = GetScrollRangeForUserInputEvents();
5776 if (mVScrollbarBox) {
5777 SetCoordAttribute(mVScrollbarBox->GetContent()->AsElement(),
5778 nsGkAtoms::curpos, pt.y - scrollRange.y);
5779 if (!weakFrame.IsAlive()) {
5780 return;
5783 if (mHScrollbarBox) {
5784 SetCoordAttribute(mHScrollbarBox->GetContent()->AsElement(),
5785 nsGkAtoms::curpos, pt.x - scrollRange.x);
5786 if (!weakFrame.IsAlive()) {
5787 return;
5791 mFrameIsUpdatingScrollbar = false;
5794 void nsHTMLScrollFrame::CurPosAttributeChangedInternal(nsIContent* aContent,
5795 bool aDoScroll) {
5796 NS_ASSERTION(aContent, "aContent must not be null");
5797 NS_ASSERTION((mHScrollbarBox && mHScrollbarBox->GetContent() == aContent) ||
5798 (mVScrollbarBox && mVScrollbarBox->GetContent() == aContent),
5799 "unexpected child");
5800 MOZ_ASSERT(aContent->IsElement());
5802 // Attribute changes on the scrollbars happen in one of three ways:
5803 // 1) The scrollbar changed the attribute in response to some user event
5804 // 2) We changed the attribute in response to a ScrollPositionDidChange
5805 // callback from the scrolling view
5806 // 3) We changed the attribute to adjust the scrollbars for the start
5807 // of a smooth scroll operation
5809 // In cases 2 and 3 we do not need to scroll because we're just
5810 // updating our scrollbar.
5811 if (mFrameIsUpdatingScrollbar) {
5812 return;
5815 nsRect scrollRange = GetVisualScrollRange();
5817 nsPoint current = GetScrollPosition() - scrollRange.TopLeft();
5819 if (gfxPlatform::UseDesktopZoomingScrollbars()) {
5820 scrollRange = GetScrollRangeForUserInputEvents();
5821 current = GetVisualViewportOffset() - scrollRange.TopLeft();
5824 nsPoint dest;
5825 nsRect allowedRange;
5826 dest.x = GetCoordAttribute(mHScrollbarBox, nsGkAtoms::curpos, current.x,
5827 &allowedRange.x, &allowedRange.width);
5828 dest.y = GetCoordAttribute(mVScrollbarBox, nsGkAtoms::curpos, current.y,
5829 &allowedRange.y, &allowedRange.height);
5830 current += scrollRange.TopLeft();
5831 dest += scrollRange.TopLeft();
5832 allowedRange += scrollRange.TopLeft();
5834 // Don't try to scroll if we're already at an acceptable place.
5835 // Don't call Contains here since Contains returns false when the point is
5836 // on the bottom or right edge of the rectangle.
5837 if (allowedRange.ClampPoint(current) == current) {
5838 return;
5841 if (mScrollbarActivity &&
5842 (mHasHorizontalScrollbar || mHasVerticalScrollbar)) {
5843 RefPtr<ScrollbarActivity> scrollbarActivity(mScrollbarActivity);
5844 scrollbarActivity->ActivityOccurred();
5847 const bool isSmooth = aContent->AsElement()->HasAttr(nsGkAtoms::smooth);
5848 if (isSmooth) {
5849 // Make sure an attribute-setting callback occurs even if the view
5850 // didn't actually move yet. We need to make sure other listeners
5851 // see that the scroll position is not (yet) what they thought it
5852 // was.
5853 AutoWeakFrame weakFrame(this);
5854 UpdateScrollbarPosition();
5855 if (!weakFrame.IsAlive()) {
5856 return;
5860 if (aDoScroll) {
5861 ScrollToWithOrigin(dest, &allowedRange,
5862 ScrollOperationParams{
5863 isSmooth ? ScrollMode::Smooth : ScrollMode::Instant,
5864 ScrollOrigin::Scrollbars});
5866 // 'this' might be destroyed here
5869 /* ============= Scroll events ========== */
5871 nsHTMLScrollFrame::ScrollEvent::ScrollEvent(nsHTMLScrollFrame* aHelper,
5872 bool aDelayed)
5873 : Runnable("nsHTMLScrollFrame::ScrollEvent"), mHelper(aHelper) {
5874 mHelper->PresContext()->RefreshDriver()->PostScrollEvent(this, aDelayed);
5877 // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
5878 MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
5879 nsHTMLScrollFrame::ScrollEvent::Run() {
5880 if (mHelper) {
5881 mHelper->FireScrollEvent();
5883 return NS_OK;
5886 nsHTMLScrollFrame::ScrollEndEvent::ScrollEndEvent(nsHTMLScrollFrame* aHelper)
5887 : Runnable("nsHTMLScrollFrame::ScrollEndEvent"), mHelper(aHelper) {
5888 mHelper->PresContext()->RefreshDriver()->PostScrollEvent(this);
5891 MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
5892 nsHTMLScrollFrame::ScrollEndEvent::Run() {
5893 if (mHelper) {
5894 mHelper->FireScrollEndEvent();
5896 return NS_OK;
5899 void nsHTMLScrollFrame::FireScrollEvent() {
5900 RefPtr<nsIContent> content = GetContent();
5901 RefPtr<nsPresContext> presContext = PresContext();
5902 AUTO_PROFILER_TRACING_MARKER_DOCSHELL("Paint", "FireScrollEvent", GRAPHICS,
5903 presContext->GetDocShell());
5905 MOZ_ASSERT(mScrollEvent);
5906 mScrollEvent->Revoke();
5907 mScrollEvent = nullptr;
5909 // If event handling is suppressed, keep posting the scroll event to the
5910 // refresh driver until it is unsuppressed. The event is marked as delayed so
5911 // that the refresh driver does not continue ticking.
5912 if (content->GetComposedDoc() &&
5913 content->GetComposedDoc()->EventHandlingSuppressed()) {
5914 content->GetComposedDoc()->SetHasDelayedRefreshEvent();
5915 PostScrollEvent(/* aDelayed = */ true);
5916 return;
5919 bool oldProcessing = mProcessingScrollEvent;
5920 AutoWeakFrame weakFrame(this);
5921 auto RestoreProcessingScrollEvent = mozilla::MakeScopeExit([&] {
5922 if (weakFrame.IsAlive()) { // Otherwise `this` will be dead too.
5923 mProcessingScrollEvent = oldProcessing;
5927 mProcessingScrollEvent = true;
5929 WidgetGUIEvent event(true, eScroll, nullptr);
5930 nsEventStatus status = nsEventStatus_eIgnore;
5931 // Fire viewport scroll events at the document (where they
5932 // will bubble to the window)
5933 mozilla::layers::ScrollLinkedEffectDetector detector(
5934 content->GetComposedDoc(),
5935 presContext->RefreshDriver()->MostRecentRefresh());
5936 if (mIsRoot) {
5937 if (RefPtr<Document> doc = content->GetUncomposedDoc()) {
5938 EventDispatcher::Dispatch(doc, presContext, &event, nullptr, &status);
5940 } else {
5941 // scroll events fired at elements don't bubble (although scroll events
5942 // fired at documents do, to the window)
5943 event.mFlags.mBubbles = false;
5944 EventDispatcher::Dispatch(content, presContext, &event, nullptr, &status);
5948 void nsHTMLScrollFrame::PostScrollEvent(bool aDelayed) {
5949 if (mScrollEvent) {
5950 return;
5953 // The ScrollEvent constructor registers itself with the refresh driver.
5954 mScrollEvent = new ScrollEvent(this, aDelayed);
5957 // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
5958 MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
5959 nsHTMLScrollFrame::AsyncScrollPortEvent::Run() {
5960 return mHelper ? mHelper->FireScrollPortEvent() : NS_OK;
5963 void nsHTMLScrollFrame::PostOverflowEvent() {
5964 if (mAsyncScrollPortEvent.IsPending()) {
5965 return;
5968 OverflowState overflowState = GetOverflowState();
5970 bool newVerticalOverflow = !!(overflowState & OverflowState::Vertical);
5971 bool vertChanged = mVerticalOverflow != newVerticalOverflow;
5973 bool newHorizontalOverflow = !!(overflowState & OverflowState::Horizontal);
5974 bool horizChanged = mHorizontalOverflow != newHorizontalOverflow;
5976 if (!vertChanged && !horizChanged) {
5977 return;
5980 nsRootPresContext* rpc = PresContext()->GetRootPresContext();
5981 if (!rpc) {
5982 return;
5985 mAsyncScrollPortEvent = new AsyncScrollPortEvent(this);
5986 rpc->AddWillPaintObserver(mAsyncScrollPortEvent.get());
5989 nsIFrame* nsHTMLScrollFrame::GetFrameForStyle() const {
5990 nsIFrame* styleFrame = nullptr;
5991 if (mIsRoot) {
5992 if (const Element* rootElement =
5993 PresContext()->Document()->GetRootElement()) {
5994 styleFrame = rootElement->GetPrimaryFrame();
5996 } else {
5997 styleFrame = const_cast<nsHTMLScrollFrame*>(this);
6000 return styleFrame;
6003 bool nsHTMLScrollFrame::NeedsScrollSnap() const {
6004 nsIFrame* scrollSnapFrame = GetFrameForStyle();
6005 if (!scrollSnapFrame) {
6006 return false;
6008 return scrollSnapFrame->StyleDisplay()->mScrollSnapType.strictness !=
6009 StyleScrollSnapStrictness::None;
6012 nsSize nsHTMLScrollFrame::GetSnapportSize() const {
6013 nsRect snapport = GetScrollPortRect();
6014 nsMargin scrollPadding = GetScrollPadding();
6015 snapport.Deflate(scrollPadding);
6016 return snapport.Size();
6019 bool nsHTMLScrollFrame::IsScrollbarOnRight() const {
6020 nsPresContext* presContext = PresContext();
6022 // The position of the scrollbar in top-level windows depends on the pref
6023 // layout.scrollbar.side. For non-top-level elements, it depends only on the
6024 // directionaliy of the element (equivalent to a value of "1" for the pref).
6025 if (!mIsRoot) {
6026 return IsPhysicalLTR();
6028 switch (presContext->GetCachedIntPref(kPresContext_ScrollbarSide)) {
6029 default:
6030 case 0: // UI directionality
6031 return presContext->GetCachedIntPref(kPresContext_BidiDirection) ==
6032 IBMBIDI_TEXTDIRECTION_LTR;
6033 case 1: // Document / content directionality
6034 return IsPhysicalLTR();
6035 case 2: // Always right
6036 return true;
6037 case 3: // Always left
6038 return false;
6042 bool nsHTMLScrollFrame::IsScrollingActive() const {
6043 const nsStyleDisplay* disp = StyleDisplay();
6044 if (disp->mWillChange.bits & StyleWillChangeBits::SCROLL) {
6045 return true;
6048 nsIContent* content = GetContent();
6049 return mHasBeenScrolledRecently || IsAlwaysActive() ||
6050 DisplayPortUtils::HasDisplayPort(content) ||
6051 nsContentUtils::HasScrollgrab(content);
6054 void nsHTMLScrollFrame::FinishReflowForScrollbar(Element* aElement,
6055 nscoord aMinXY, nscoord aMaxXY,
6056 nscoord aCurPosXY,
6057 nscoord aPageIncrement,
6058 nscoord aIncrement) {
6059 // Scrollbars assume zero is the minimum position, so translate for them.
6060 SetCoordAttribute(aElement, nsGkAtoms::curpos, aCurPosXY - aMinXY);
6061 SetScrollbarEnabled(aElement, aMaxXY - aMinXY);
6062 SetCoordAttribute(aElement, nsGkAtoms::maxpos, aMaxXY - aMinXY);
6063 SetCoordAttribute(aElement, nsGkAtoms::pageincrement, aPageIncrement);
6064 SetCoordAttribute(aElement, nsGkAtoms::increment, aIncrement);
6067 class MOZ_RAII AutoMinimumScaleSizeChangeDetector final {
6068 public:
6069 explicit AutoMinimumScaleSizeChangeDetector(
6070 nsHTMLScrollFrame* ansHTMLScrollFrame)
6071 : mHelper(ansHTMLScrollFrame) {
6072 MOZ_ASSERT(mHelper);
6073 MOZ_ASSERT(mHelper->mIsRoot);
6075 mPreviousMinimumScaleSize = ansHTMLScrollFrame->mMinimumScaleSize;
6076 mPreviousIsUsingMinimumScaleSize =
6077 ansHTMLScrollFrame->mIsUsingMinimumScaleSize;
6079 ~AutoMinimumScaleSizeChangeDetector() {
6080 if (mPreviousMinimumScaleSize != mHelper->mMinimumScaleSize ||
6081 mPreviousIsUsingMinimumScaleSize != mHelper->mIsUsingMinimumScaleSize) {
6082 mHelper->mMinimumScaleSizeChanged = true;
6086 private:
6087 nsHTMLScrollFrame* mHelper;
6089 nsSize mPreviousMinimumScaleSize;
6090 bool mPreviousIsUsingMinimumScaleSize;
6093 nsSize nsHTMLScrollFrame::TrueOuterSize(nsDisplayListBuilder* aBuilder) const {
6094 if (!PresShell()->UsesMobileViewportSizing()) {
6095 return GetSize();
6098 RefPtr<MobileViewportManager> manager =
6099 PresShell()->GetMobileViewportManager();
6100 MOZ_ASSERT(manager);
6102 LayoutDeviceIntSize displaySize = manager->DisplaySize();
6104 MOZ_ASSERT(aBuilder);
6105 // In case of WebRender, we expand the outer size to include the dynamic
6106 // toolbar area here.
6107 // In case of non WebRender, we expand the size dynamically in
6108 // MoveScrollbarForLayerMargin in AsyncCompositionManager.cpp.
6109 WebRenderLayerManager* layerManager = aBuilder->GetWidgetLayerManager();
6110 if (layerManager) {
6111 displaySize.height += ViewAs<LayoutDevicePixel>(
6112 PresContext()->GetDynamicToolbarMaxHeight(),
6113 PixelCastJustification::LayoutDeviceIsScreenForBounds);
6116 return LayoutDeviceSize::ToAppUnits(displaySize,
6117 PresContext()->AppUnitsPerDevPixel());
6120 void nsHTMLScrollFrame::UpdateMinimumScaleSize(
6121 const nsRect& aScrollableOverflow, const nsSize& aICBSize) {
6122 MOZ_ASSERT(mIsRoot);
6124 AutoMinimumScaleSizeChangeDetector minimumScaleSizeChangeDetector(this);
6126 mIsUsingMinimumScaleSize = false;
6128 if (!PresShell()->UsesMobileViewportSizing()) {
6129 return;
6132 nsPresContext* pc = PresContext();
6133 MOZ_ASSERT(pc->IsRootContentDocumentCrossProcess(),
6134 "The pres context should be for the root content document");
6136 RefPtr<MobileViewportManager> manager =
6137 PresShell()->GetMobileViewportManager();
6138 MOZ_ASSERT(manager);
6140 ScreenIntSize displaySize = ViewAs<ScreenPixel>(
6141 manager->DisplaySize(),
6142 PixelCastJustification::LayoutDeviceIsScreenForBounds);
6143 if (displaySize.width == 0 || displaySize.height == 0) {
6144 return;
6146 if (aScrollableOverflow.IsEmpty()) {
6147 // Bail if the scrollable overflow rect is empty, as we're going to be
6148 // dividing by it.
6149 return;
6152 Document* doc = pc->Document();
6153 MOZ_ASSERT(doc, "The document should be valid");
6154 if (doc->GetFullscreenElement()) {
6155 // Don't use the minimum scale size in the case of fullscreen state.
6156 // FIXME: 1508177: We will no longer need this.
6157 return;
6160 nsViewportInfo viewportInfo = doc->GetViewportInfo(displaySize);
6161 if (!viewportInfo.IsZoomAllowed()) {
6162 // Don't apply the minimum scale size if user-scalable=no is specified.
6163 return;
6166 // The intrinsic minimum scale is the scale that fits the entire content
6167 // width into the visual viewport.
6168 CSSToScreenScale intrinsicMinScale(
6169 displaySize.width / CSSRect::FromAppUnits(aScrollableOverflow).XMost());
6171 // The scale used to compute the minimum-scale size is the larger of the
6172 // intrinsic minimum and the min-scale from the meta viewport tag.
6173 CSSToScreenScale minScale =
6174 std::max(intrinsicMinScale, viewportInfo.GetMinZoom());
6176 // The minimum-scale size is the size of the visual viewport when zoomed
6177 // to be the minimum scale.
6178 mMinimumScaleSize = CSSSize::ToAppUnits(ScreenSize(displaySize) / minScale);
6180 // Ensure the minimum-scale size is never smaller than the ICB size.
6181 // That could happen if a page has a meta viewport tag with large explicitly
6182 // specified viewport dimensions (making the ICB large) and also a large
6183 // minimum scale (making the min-scale size small).
6184 mMinimumScaleSize = Max(aICBSize, mMinimumScaleSize);
6186 mIsUsingMinimumScaleSize = true;
6189 bool nsHTMLScrollFrame::ReflowFinished() {
6190 mPostedReflowCallback = false;
6192 TryScheduleScrollAnimations();
6194 if (mIsRoot) {
6195 if (mMinimumScaleSizeChanged && PresShell()->UsesMobileViewportSizing() &&
6196 !PresShell()->IsResolutionUpdatedByApz()) {
6197 RefPtr<MobileViewportManager> manager =
6198 PresShell()->GetMobileViewportManager();
6199 MOZ_ASSERT(manager);
6201 manager->ShrinkToDisplaySizeIfNeeded();
6202 mMinimumScaleSizeChanged = false;
6205 if (!UsesOverlayScrollbars()) {
6206 // Layout scrollbars may have added or removed during reflow, so let's
6207 // update the visual viewport accordingly. Note that this may be a no-op
6208 // because we might have recomputed the visual viewport size during the
6209 // reflow itself, just before laying out the fixed-pos items. But there
6210 // might be cases where that code doesn't run, so this is a sort of
6211 // backstop to ensure we do that recomputation.
6212 if (RefPtr<MobileViewportManager> manager =
6213 PresShell()->GetMobileViewportManager()) {
6214 manager->UpdateVisualViewportSizeForPotentialScrollbarChange();
6218 #if defined(MOZ_WIDGET_ANDROID)
6219 const bool hasVerticalOverflow =
6220 GetOverflowState() & OverflowState::Vertical &&
6221 GetScrollStyles().mVertical != StyleOverflow::Hidden;
6222 if (!mFirstReflow && mHasVerticalOverflowForDynamicToolbar &&
6223 !hasVerticalOverflow) {
6224 PresShell()->MaybeNotifyShowDynamicToolbar();
6226 mHasVerticalOverflowForDynamicToolbar = hasVerticalOverflow;
6227 #endif // defined(MOZ_WIDGET_ANDROID)
6230 bool doScroll = true;
6231 if (IsSubtreeDirty()) {
6232 // We will get another call after the next reflow and scrolling
6233 // later is less janky.
6234 doScroll = false;
6237 if (mFirstReflow) {
6238 nsPoint currentScrollPos = GetScrollPosition();
6239 if (!mScrollUpdates.IsEmpty() &&
6240 mScrollUpdates.LastElement().GetOrigin() == ScrollOrigin::None &&
6241 currentScrollPos != nsPoint()) {
6242 // With frame reconstructions, the reconstructed frame may have a nonzero
6243 // scroll position by the end of the reflow, but without going through
6244 // RestoreState. In particular this can happen with RTL XUL scrollframes,
6245 // see https://bugzilla.mozilla.org/show_bug.cgi?id=1664638#c14.
6246 // Upon construction, the nsHTMLScrollFrame constructor will have inserted
6247 // a ScrollPositionUpdate into mScrollUpdates with origin None and a zero
6248 // scroll position, but here we update that to hold the correct scroll
6249 // position. Otherwise APZ may end up resetting the scroll position to
6250 // zero incorrectly. If we ever hit this codepath, it must be on a reflow
6251 // immediately following the scrollframe construction, so there should be
6252 // exactly one ScrollPositionUpdate in mScrollUpdates.
6253 MOZ_ASSERT(mScrollUpdates.Length() == 1);
6254 MOZ_ASSERT(mScrollUpdates.LastElement().GetGeneration() ==
6255 mScrollGeneration);
6256 MOZ_ASSERT(mScrollUpdates.LastElement().GetDestination() == CSSPoint());
6257 SCROLLRESTORE_LOG("%p: updating initial SPU to pos %s\n", this,
6258 ToString(currentScrollPos).c_str());
6259 mScrollUpdates.Clear();
6260 AppendScrollUpdate(
6261 ScrollPositionUpdate::NewScrollframe(currentScrollPos));
6264 mFirstReflow = false;
6267 nsAutoScriptBlocker scriptBlocker;
6269 if (mReclampVVOffsetInReflowFinished) {
6270 MOZ_ASSERT(mIsRoot && PresShell()->IsVisualViewportOffsetSet());
6271 mReclampVVOffsetInReflowFinished = false;
6272 AutoWeakFrame weakFrame(this);
6273 PresShell()->SetVisualViewportOffset(PresShell()->GetVisualViewportOffset(),
6274 GetScrollPosition());
6275 NS_ENSURE_TRUE(weakFrame.IsAlive(), false);
6278 if (doScroll) {
6279 ScrollToRestoredPosition();
6281 // Clamp current scroll position to new bounds. Normally this won't
6282 // do anything.
6283 nsPoint currentScrollPos = GetScrollPosition();
6284 ScrollToImpl(currentScrollPos, nsRect(currentScrollPos, nsSize(0, 0)),
6285 ScrollOrigin::Clamp);
6286 if (ScrollAnimationState().isEmpty()) {
6287 // We need to have mDestination track the current scroll position,
6288 // in case it falls outside the new reflow area. mDestination is used
6289 // by ScrollBy as its starting position.
6290 mDestination = GetScrollPosition();
6294 if (!mUpdateScrollbarAttributes) {
6295 return false;
6297 mUpdateScrollbarAttributes = false;
6299 // Update scrollbar attributes.
6300 if (mMayHaveDirtyFixedChildren) {
6301 mMayHaveDirtyFixedChildren = false;
6302 nsIFrame* parentFrame = GetParent();
6303 for (nsIFrame* fixedChild =
6304 parentFrame->GetChildList(FrameChildListID::Fixed).FirstChild();
6305 fixedChild; fixedChild = fixedChild->GetNextSibling()) {
6306 // force a reflow of the fixed child
6307 PresShell()->FrameNeedsReflow(fixedChild, IntrinsicDirty::None,
6308 NS_FRAME_HAS_DIRTY_CHILDREN);
6312 // Suppress handling of the curpos attribute changes we make here.
6313 NS_ASSERTION(!mFrameIsUpdatingScrollbar, "We shouldn't be reentering here");
6314 mFrameIsUpdatingScrollbar = true;
6316 // FIXME(emilio): Why this instead of mHScrollbarContent / mVScrollbarContent?
6317 RefPtr<Element> vScroll =
6318 mVScrollbarBox ? mVScrollbarBox->GetContent()->AsElement() : nullptr;
6319 RefPtr<Element> hScroll =
6320 mHScrollbarBox ? mHScrollbarBox->GetContent()->AsElement() : nullptr;
6322 // Note, in some cases this may get deleted while finishing reflow
6323 // for scrollbars. XXXmats is this still true now that we have a script
6324 // blocker in this scope? (if not, remove the weak frame checks below).
6325 if (vScroll || hScroll) {
6326 nsSize visualViewportSize = GetVisualViewportSize();
6327 nsRect scrollRange = GetVisualScrollRange();
6328 nsPoint scrollPos = GetScrollPosition();
6329 nsSize lineScrollAmount = GetLineScrollAmount();
6331 if (gfxPlatform::UseDesktopZoomingScrollbars()) {
6332 scrollRange = GetScrollRangeForUserInputEvents();
6333 scrollPos = GetVisualViewportOffset();
6336 // If modifying the logic here, be sure to modify the corresponding
6337 // compositor-side calculation in ScrollThumbUtils::ApplyTransformForAxis().
6338 AutoWeakFrame weakFrame(this);
6339 if (vScroll) {
6340 const double kScrollMultiplier =
6341 StaticPrefs::toolkit_scrollbox_verticalScrollDistance();
6342 nscoord increment = lineScrollAmount.height * kScrollMultiplier;
6343 // We normally use (visualViewportSize.height - increment) for height of
6344 // page scrolling. However, it is too small when increment is very large.
6345 // (If increment is larger than visualViewportSize.height, direction of
6346 // scrolling will be opposite). To avoid it, we use
6347 // (float(visualViewportSize.height) * 0.8) as lower bound value of height
6348 // of page scrolling. (bug 383267)
6349 // XXX shouldn't we use GetPageScrollAmount here?
6350 nscoord pageincrement = nscoord(visualViewportSize.height - increment);
6351 nscoord pageincrementMin =
6352 nscoord(float(visualViewportSize.height) * 0.8);
6353 FinishReflowForScrollbar(
6354 vScroll, scrollRange.y, scrollRange.YMost(), scrollPos.y,
6355 std::max(pageincrement, pageincrementMin), increment);
6357 if (hScroll) {
6358 const double kScrollMultiplier =
6359 StaticPrefs::toolkit_scrollbox_horizontalScrollDistance();
6360 nscoord increment = lineScrollAmount.width * kScrollMultiplier;
6361 FinishReflowForScrollbar(
6362 hScroll, scrollRange.x, scrollRange.XMost(), scrollPos.x,
6363 nscoord(float(visualViewportSize.width) * 0.8), increment);
6365 NS_ENSURE_TRUE(weakFrame.IsAlive(), false);
6368 mFrameIsUpdatingScrollbar = false;
6369 // We used to rely on the curpos attribute changes above to scroll the
6370 // view. However, for scrolling to the left of the viewport, we
6371 // rescale the curpos attribute, which means that operations like
6372 // resizing the window while it is scrolled all the way to the left
6373 // hold the curpos attribute constant at 0 while still requiring
6374 // scrolling. So we suppress the effect of the changes above with
6375 // mFrameIsUpdatingScrollbar and call CurPosAttributeChanged here.
6376 // (It actually even works some of the time without this, thanks to
6377 // nsSliderFrame::AttributeChanged's handling of maxpos, but not when
6378 // we hide the scrollbar on a large size change, such as
6379 // maximization.)
6380 if (!mHScrollbarBox && !mVScrollbarBox) {
6381 return false;
6383 CurPosAttributeChangedInternal(
6384 mVScrollbarBox ? mVScrollbarBox->GetContent()->AsElement()
6385 : mHScrollbarBox->GetContent()->AsElement(),
6386 doScroll);
6387 return doScroll;
6390 void nsHTMLScrollFrame::ReflowCallbackCanceled() {
6391 mPostedReflowCallback = false;
6394 bool nsHTMLScrollFrame::ComputeCustomOverflow(OverflowAreas& aOverflowAreas) {
6395 ScrollStyles ss = GetScrollStyles();
6397 // Reflow when the change in overflow leads to one of our scrollbars
6398 // changing or might require repositioning the scrolled content due to
6399 // reduced extents.
6400 nsRect scrolledRect = GetScrolledRect();
6401 ScrollDirections overflowChange =
6402 GetOverflowChange(scrolledRect, mPrevScrolledRect);
6403 mPrevScrolledRect = scrolledRect;
6405 bool needReflow = false;
6406 nsPoint scrollPosition = GetScrollPosition();
6407 if (overflowChange.contains(ScrollDirection::eHorizontal)) {
6408 if (ss.mHorizontal != StyleOverflow::Hidden || scrollPosition.x) {
6409 needReflow = true;
6412 if (overflowChange.contains(ScrollDirection::eVertical)) {
6413 if (ss.mVertical != StyleOverflow::Hidden || scrollPosition.y) {
6414 needReflow = true;
6418 if (needReflow) {
6419 // If there are scrollbars, or we're not at the beginning of the pane,
6420 // the scroll position may change. In this case, mark the frame as
6421 // needing reflow. Don't use NS_FRAME_IS_DIRTY as dirty as that means
6422 // we have to reflow the frame and all its descendants, and we don't
6423 // have to do that here. Only this frame needs to be reflowed.
6424 PresShell()->FrameNeedsReflow(this, IntrinsicDirty::None,
6425 NS_FRAME_HAS_DIRTY_CHILDREN);
6426 // Ensure that next time nsHTMLScrollFrame::Reflow runs, we don't skip
6427 // updating the scrollbars. (Because the overflow area of the scrolled
6428 // frame has probably just been updated, Reflow won't see it change.)
6429 mSkippedScrollbarLayout = true;
6430 return false; // reflowing will update overflow
6432 PostOverflowEvent();
6433 return nsContainerFrame::ComputeCustomOverflow(aOverflowAreas);
6436 void nsHTMLScrollFrame::UpdateSticky() {
6437 StickyScrollContainer* ssc =
6438 StickyScrollContainer::GetStickyScrollContainerForScrollFrame(this);
6439 if (ssc) {
6440 ssc->UpdatePositions(GetScrollPosition(), this);
6444 void nsHTMLScrollFrame::UpdatePrevScrolledRect() {
6445 // The layout scroll range is determinated by the scrolled rect and the scroll
6446 // port, so if the scrolled rect is updated, we may have to schedule the
6447 // associated scroll-driven animations' restyles.
6448 nsRect currScrolledRect = GetScrolledRect();
6449 if (!currScrolledRect.IsEqualEdges(mPrevScrolledRect)) {
6450 mMayScheduleScrollAnimations = true;
6452 mPrevScrolledRect = currScrolledRect;
6455 void nsHTMLScrollFrame::AdjustScrollbarRectForResizer(
6456 nsIFrame* aFrame, nsPresContext* aPresContext, nsRect& aRect,
6457 bool aHasResizer, ScrollDirection aDirection) {
6458 if ((aDirection == ScrollDirection::eVertical ? aRect.width : aRect.height) ==
6459 0) {
6460 return;
6463 // if a content resizer is present, use its size. Otherwise, check if the
6464 // widget has a resizer.
6465 nsRect resizerRect;
6466 if (aHasResizer) {
6467 resizerRect = mResizerBox->GetRect();
6468 } else {
6469 nsPoint offset;
6470 nsIWidget* widget = aFrame->GetNearestWidget(offset);
6471 LayoutDeviceIntRect widgetRect;
6472 if (!widget || !widget->ShowsResizeIndicator(&widgetRect)) {
6473 return;
6476 resizerRect =
6477 nsRect(aPresContext->DevPixelsToAppUnits(widgetRect.x) - offset.x,
6478 aPresContext->DevPixelsToAppUnits(widgetRect.y) - offset.y,
6479 aPresContext->DevPixelsToAppUnits(widgetRect.width),
6480 aPresContext->DevPixelsToAppUnits(widgetRect.height));
6483 if (resizerRect.Contains(aRect.BottomRight() - nsPoint(1, 1))) {
6484 switch (aDirection) {
6485 case ScrollDirection::eVertical:
6486 aRect.height = std::max(0, resizerRect.y - aRect.y);
6487 break;
6488 case ScrollDirection::eHorizontal:
6489 aRect.width = std::max(0, resizerRect.x - aRect.x);
6490 break;
6492 } else if (resizerRect.Contains(aRect.BottomLeft() + nsPoint(1, -1))) {
6493 switch (aDirection) {
6494 case ScrollDirection::eVertical:
6495 aRect.height = std::max(0, resizerRect.y - aRect.y);
6496 break;
6497 case ScrollDirection::eHorizontal: {
6498 nscoord xmost = aRect.XMost();
6499 aRect.x = std::max(aRect.x, resizerRect.XMost());
6500 aRect.width = xmost - aRect.x;
6501 break;
6507 static void AdjustOverlappingScrollbars(nsRect& aVRect, nsRect& aHRect) {
6508 if (aVRect.IsEmpty() || aHRect.IsEmpty()) return;
6510 const nsRect oldVRect = aVRect;
6511 const nsRect oldHRect = aHRect;
6512 if (oldVRect.Contains(oldHRect.BottomRight() - nsPoint(1, 1))) {
6513 aHRect.width = std::max(0, oldVRect.x - oldHRect.x);
6514 } else if (oldVRect.Contains(oldHRect.BottomLeft() - nsPoint(0, 1))) {
6515 nscoord overlap = std::min(oldHRect.width, oldVRect.XMost() - oldHRect.x);
6516 aHRect.x += overlap;
6517 aHRect.width -= overlap;
6519 if (oldHRect.Contains(oldVRect.BottomRight() - nsPoint(1, 1))) {
6520 aVRect.height = std::max(0, oldHRect.y - oldVRect.y);
6524 void nsHTMLScrollFrame::LayoutScrollbarPartAtRect(
6525 const ScrollReflowInput& aState, ReflowInput& aKidReflowInput,
6526 const nsRect& aRect) {
6527 nsPresContext* pc = PresContext();
6528 nsIFrame* kid = aKidReflowInput.mFrame;
6529 const auto wm = kid->GetWritingMode();
6530 ReflowOutput desiredSize(wm);
6531 MOZ_ASSERT(!wm.IsVertical(),
6532 "Scrollbar parts should have writing-mode: initial");
6533 MOZ_ASSERT(!wm.IsInlineReversed(),
6534 "Scrollbar parts should have writing-mode: initial");
6535 // XXX Maybe get a meaningful container size or something. Shouldn't matter
6536 // given our asserts above.
6537 const nsSize containerSize;
6538 aKidReflowInput.SetComputedISize(aRect.Width());
6539 aKidReflowInput.SetComputedBSize(aRect.Height());
6541 const LogicalPoint pos(wm, aRect.TopLeft(), containerSize);
6542 const auto flags = ReflowChildFlags::Default;
6543 nsReflowStatus status;
6544 ReflowOutput kidDesiredSize(wm);
6545 ReflowChild(kid, pc, kidDesiredSize, aKidReflowInput, wm, pos, containerSize,
6546 flags, status);
6547 FinishReflowChild(kid, pc, kidDesiredSize, &aKidReflowInput, wm, pos,
6548 containerSize, flags);
6551 void nsHTMLScrollFrame::LayoutScrollbars(ScrollReflowInput& aState,
6552 const nsRect& aInsideBorderArea,
6553 const nsRect& aOldScrollPort) {
6554 NS_ASSERTION(!mSuppressScrollbarUpdate, "This should have been suppressed");
6556 const bool scrollbarOnLeft = !IsScrollbarOnRight();
6557 const bool overlayScrollbars = UsesOverlayScrollbars();
6558 const bool overlayScrollBarsOnRoot = overlayScrollbars && mIsRoot;
6559 const bool showVScrollbar = mVScrollbarBox && mHasVerticalScrollbar;
6560 const bool showHScrollbar = mHScrollbarBox && mHasHorizontalScrollbar;
6562 nsSize compositionSize = mScrollPort.Size();
6563 if (overlayScrollBarsOnRoot) {
6564 compositionSize = nsLayoutUtils::CalculateCompositionSizeForFrame(
6565 this, false, &compositionSize);
6568 nsPresContext* presContext = mScrolledFrame->PresContext();
6569 nsRect vRect;
6570 if (showVScrollbar) {
6571 vRect.height =
6572 overlayScrollBarsOnRoot ? compositionSize.height : mScrollPort.height;
6573 vRect.y = mScrollPort.y;
6574 if (scrollbarOnLeft) {
6575 vRect.width = mScrollPort.x - aInsideBorderArea.x;
6576 vRect.x = aInsideBorderArea.x;
6577 } else {
6578 vRect.width = aInsideBorderArea.XMost() - mScrollPort.XMost();
6579 vRect.x = mScrollPort.x + compositionSize.width;
6581 if (overlayScrollbars || mOnlyNeedVScrollbarToScrollVVInsideLV) {
6582 const nscoord width = aState.VScrollbarPrefWidth();
6583 // There is no space reserved for the layout scrollbar, it is currently
6584 // not visible because it is positioned just outside the scrollport. But
6585 // we know that it needs to be made visible so we shift it back in.
6586 vRect.width += width;
6587 if (!scrollbarOnLeft) {
6588 vRect.x -= width;
6593 nsRect hRect;
6594 if (showHScrollbar) {
6595 hRect.width =
6596 overlayScrollBarsOnRoot ? compositionSize.width : mScrollPort.width;
6597 hRect.x = mScrollPort.x;
6598 hRect.height = aInsideBorderArea.YMost() - mScrollPort.YMost();
6599 hRect.y = mScrollPort.y + compositionSize.height;
6601 if (overlayScrollbars || mOnlyNeedHScrollbarToScrollVVInsideLV) {
6602 const nscoord height = aState.HScrollbarPrefHeight();
6603 hRect.height += height;
6604 // There is no space reserved for the layout scrollbar, it is currently
6605 // not visible because it is positioned just outside the scrollport. But
6606 // we know that it needs to be made visible so we shift it back in.
6607 hRect.y -= height;
6611 const bool hasVisualOnlyScrollbarsOnBothDirections =
6612 !overlayScrollbars && showHScrollbar &&
6613 mOnlyNeedHScrollbarToScrollVVInsideLV && showVScrollbar &&
6614 mOnlyNeedVScrollbarToScrollVVInsideLV;
6615 nsPresContext* pc = PresContext();
6617 // place the scrollcorner
6618 if (mScrollCornerBox) {
6619 nsRect r(0, 0, 0, 0);
6620 if (scrollbarOnLeft) {
6621 // scrollbar (if any) on left
6622 r.width = showVScrollbar ? mScrollPort.x - aInsideBorderArea.x : 0;
6623 r.x = aInsideBorderArea.x;
6624 } else {
6625 // scrollbar (if any) on right
6626 r.width =
6627 showVScrollbar ? aInsideBorderArea.XMost() - mScrollPort.XMost() : 0;
6628 r.x = aInsideBorderArea.XMost() - r.width;
6630 NS_ASSERTION(r.width >= 0, "Scroll area should be inside client rect");
6632 if (showHScrollbar) {
6633 // scrollbar (if any) on bottom
6634 // Note we don't support the horizontal scrollbar at the top side.
6635 r.height = aInsideBorderArea.YMost() - mScrollPort.YMost();
6636 NS_ASSERTION(r.height >= 0, "Scroll area should be inside client rect");
6638 r.y = aInsideBorderArea.YMost() - r.height;
6640 // If we have layout scrollbars and both scrollbars are present and both are
6641 // only needed to scroll the VV inside the LV then we need a scrollcorner
6642 // but the above calculation will result in an empty rect, so adjust it.
6643 if (r.IsEmpty() && hasVisualOnlyScrollbarsOnBothDirections) {
6644 r.width = vRect.width;
6645 r.height = hRect.height;
6646 r.x = scrollbarOnLeft ? mScrollPort.x : mScrollPort.XMost() - r.width;
6647 r.y = mScrollPort.YMost() - r.height;
6650 ReflowInput scrollCornerRI(
6651 pc, aState.mReflowInput, mScrollCornerBox,
6652 LogicalSize(mScrollCornerBox->GetWritingMode(), r.Size()));
6653 LayoutScrollbarPartAtRect(aState, scrollCornerRI, r);
6656 if (mResizerBox) {
6657 // If a resizer is present, get its size.
6659 // TODO(emilio): Should this really account for scrollbar-width?
6660 auto scrollbarWidth = nsLayoutUtils::StyleForScrollbar(this)
6661 ->StyleUIReset()
6662 ->ScrollbarWidth();
6663 const nscoord scrollbarSize =
6664 GetNonOverlayScrollbarSize(pc, scrollbarWidth);
6665 ReflowInput resizerRI(pc, aState.mReflowInput, mResizerBox,
6666 LogicalSize(mResizerBox->GetWritingMode()));
6667 nsSize resizerMinSize = {resizerRI.ComputedMinWidth(),
6668 resizerRI.ComputedMinHeight()};
6670 nsRect r;
6671 r.width = std::max(std::max(r.width, scrollbarSize), resizerMinSize.width);
6672 r.x = scrollbarOnLeft ? aInsideBorderArea.x
6673 : aInsideBorderArea.XMost() - r.width;
6674 r.height =
6675 std::max(std::max(r.height, scrollbarSize), resizerMinSize.height);
6676 r.y = aInsideBorderArea.YMost() - r.height;
6678 LayoutScrollbarPartAtRect(aState, resizerRI, r);
6681 // Note that AdjustScrollbarRectForResizer has to be called after the
6682 // resizer has been laid out immediately above this because it gets the rect
6683 // of the resizer frame.
6684 if (mVScrollbarBox) {
6685 AdjustScrollbarRectForResizer(this, presContext, vRect, mResizerBox,
6686 ScrollDirection::eVertical);
6688 if (mHScrollbarBox) {
6689 AdjustScrollbarRectForResizer(this, presContext, hRect, mResizerBox,
6690 ScrollDirection::eHorizontal);
6693 // Layout scrollbars can overlap at this point if they are both present and
6694 // both only needed to scroll the VV inside the LV.
6695 if (!LookAndFeel::GetInt(LookAndFeel::IntID::AllowOverlayScrollbarsOverlap) ||
6696 hasVisualOnlyScrollbarsOnBothDirections) {
6697 AdjustOverlappingScrollbars(vRect, hRect);
6699 if (mVScrollbarBox) {
6700 ReflowInput vScrollbarRI(
6701 pc, aState.mReflowInput, mVScrollbarBox,
6702 LogicalSize(mVScrollbarBox->GetWritingMode(), vRect.Size()));
6703 LayoutScrollbarPartAtRect(aState, vScrollbarRI, vRect);
6705 if (mHScrollbarBox) {
6706 ReflowInput hScrollbarRI(
6707 pc, aState.mReflowInput, mHScrollbarBox,
6708 LogicalSize(mHScrollbarBox->GetWritingMode(), hRect.Size()));
6709 LayoutScrollbarPartAtRect(aState, hScrollbarRI, hRect);
6712 // may need to update fixed position children of the viewport,
6713 // if the client area changed size because of an incremental
6714 // reflow of a descendant. (If the outer frame is dirty, the fixed
6715 // children will be re-laid out anyway)
6716 if (aOldScrollPort.Size() != mScrollPort.Size() &&
6717 !HasAnyStateBits(NS_FRAME_IS_DIRTY) && mIsRoot) {
6718 mMayHaveDirtyFixedChildren = true;
6721 // post reflow callback to modify scrollbar attributes
6722 mUpdateScrollbarAttributes = true;
6723 if (!mPostedReflowCallback) {
6724 PresShell()->PostReflowCallback(this);
6725 mPostedReflowCallback = true;
6729 #if DEBUG
6730 static bool ShellIsAlive(nsWeakPtr& aWeakPtr) {
6731 RefPtr<PresShell> presShell = do_QueryReferent(aWeakPtr);
6732 return !!presShell;
6734 #endif
6736 void nsHTMLScrollFrame::SetScrollbarEnabled(Element* aElement,
6737 nscoord aMaxPos) {
6738 DebugOnly<nsWeakPtr> weakShell(do_GetWeakReference(PresShell()));
6739 if (aMaxPos) {
6740 aElement->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true);
6741 } else {
6742 aElement->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, u"true"_ns, true);
6744 MOZ_ASSERT(ShellIsAlive(weakShell), "pres shell was destroyed by scrolling");
6747 void nsHTMLScrollFrame::SetCoordAttribute(Element* aElement, nsAtom* aAtom,
6748 nscoord aSize) {
6749 DebugOnly<nsWeakPtr> weakShell(do_GetWeakReference(PresShell()));
6750 // convert to pixels
6751 int32_t pixelSize = nsPresContext::AppUnitsToIntCSSPixels(aSize);
6753 // only set the attribute if it changed.
6755 nsAutoString newValue;
6756 newValue.AppendInt(pixelSize);
6758 if (aElement->AttrValueIs(kNameSpaceID_None, aAtom, newValue, eCaseMatters)) {
6759 return;
6762 AutoWeakFrame weakFrame(this);
6763 RefPtr<Element> kungFuDeathGrip = aElement;
6764 aElement->SetAttr(kNameSpaceID_None, aAtom, newValue, true);
6765 MOZ_ASSERT(ShellIsAlive(weakShell), "pres shell was destroyed by scrolling");
6766 if (!weakFrame.IsAlive()) {
6767 return;
6770 if (mScrollbarActivity &&
6771 (mHasHorizontalScrollbar || mHasVerticalScrollbar)) {
6772 RefPtr<ScrollbarActivity> scrollbarActivity(mScrollbarActivity);
6773 scrollbarActivity->ActivityOccurred();
6777 static void ReduceRadii(nscoord aXBorder, nscoord aYBorder, nscoord& aXRadius,
6778 nscoord& aYRadius) {
6779 // In order to ensure that the inside edge of the border has no
6780 // curvature, we need at least one of its radii to be zero.
6781 if (aXRadius <= aXBorder || aYRadius <= aYBorder) return;
6783 // For any corner where we reduce the radii, preserve the corner's shape.
6784 double ratio =
6785 std::max(double(aXBorder) / aXRadius, double(aYBorder) / aYRadius);
6786 aXRadius *= ratio;
6787 aYRadius *= ratio;
6791 * Implement an override for nsIFrame::GetBorderRadii to ensure that
6792 * the clipping region for the border radius does not clip the scrollbars.
6794 * In other words, we require that the border radius be reduced until the
6795 * inner border radius at the inner edge of the border is 0 wherever we
6796 * have scrollbars.
6798 bool nsHTMLScrollFrame::GetBorderRadii(const nsSize& aFrameSize,
6799 const nsSize& aBorderArea,
6800 Sides aSkipSides,
6801 nscoord aRadii[8]) const {
6802 if (!nsContainerFrame::GetBorderRadii(aFrameSize, aBorderArea, aSkipSides,
6803 aRadii)) {
6804 return false;
6807 // Since we can use GetActualScrollbarSizes (rather than
6808 // GetDesiredScrollbarSizes) since this doesn't affect reflow, we
6809 // probably should.
6810 nsMargin sb = GetActualScrollbarSizes();
6811 nsMargin border = GetUsedBorder();
6813 if (sb.left > 0 || sb.top > 0) {
6814 ReduceRadii(border.left, border.top, aRadii[eCornerTopLeftX],
6815 aRadii[eCornerTopLeftY]);
6818 if (sb.top > 0 || sb.right > 0) {
6819 ReduceRadii(border.right, border.top, aRadii[eCornerTopRightX],
6820 aRadii[eCornerTopRightY]);
6823 if (sb.right > 0 || sb.bottom > 0) {
6824 ReduceRadii(border.right, border.bottom, aRadii[eCornerBottomRightX],
6825 aRadii[eCornerBottomRightY]);
6828 if (sb.bottom > 0 || sb.left > 0) {
6829 ReduceRadii(border.left, border.bottom, aRadii[eCornerBottomLeftX],
6830 aRadii[eCornerBottomLeftY]);
6833 return true;
6836 static nscoord SnapCoord(nscoord aCoord, double aRes,
6837 nscoord aAppUnitsPerPixel) {
6838 if (StaticPrefs::layout_scroll_disable_pixel_alignment()) {
6839 return aCoord;
6841 double snappedToLayerPixels = NS_round((aRes * aCoord) / aAppUnitsPerPixel);
6842 return NSToCoordRoundWithClamp(snappedToLayerPixels * aAppUnitsPerPixel /
6843 aRes);
6846 nsRect nsHTMLScrollFrame::GetScrolledRect() const {
6847 nsRect result = GetUnsnappedScrolledRectInternal(
6848 mScrolledFrame->ScrollableOverflowRect(), mScrollPort.Size());
6850 #if 0
6851 // This happens often enough.
6852 if (result.width < mScrollPort.width || result.height < mScrollPort.height) {
6853 NS_WARNING("Scrolled rect smaller than scrollport?");
6855 #endif
6857 // Expand / contract the result by up to half a layer pixel so that scrolling
6858 // to the right / bottom edge does not change the layer pixel alignment of
6859 // the scrolled contents.
6861 if (result.x == 0 && result.y == 0 && result.width == mScrollPort.width &&
6862 result.height == mScrollPort.height) {
6863 // The edges that we would snap are already aligned with the scroll port,
6864 // so we can skip all the work below.
6865 return result;
6868 // For that, we first convert the scroll port and the scrolled rect to rects
6869 // relative to the reference frame, since that's the space where painting does
6870 // snapping.
6871 nsSize visualViewportSize = GetVisualViewportSize();
6872 const nsIFrame* referenceFrame =
6873 mReferenceFrameDuringPainting ? mReferenceFrameDuringPainting
6874 : nsLayoutUtils::GetReferenceFrame(
6875 const_cast<nsHTMLScrollFrame*>(this));
6876 nsPoint toReferenceFrame = GetOffsetToCrossDoc(referenceFrame);
6877 nsRect scrollPort(mScrollPort.TopLeft() + toReferenceFrame,
6878 visualViewportSize);
6879 nsRect scrolledRect = result + scrollPort.TopLeft();
6881 if (scrollPort.Overflows() || scrolledRect.Overflows()) {
6882 return result;
6885 // Now, snap the bottom right corner of both of these rects.
6886 // We snap to layer pixels, so we need to respect the layer's scale.
6887 nscoord appUnitsPerDevPixel =
6888 mScrolledFrame->PresContext()->AppUnitsPerDevPixel();
6889 MatrixScales scale = GetPaintedLayerScaleForFrame(
6890 mScrolledFrame, /* aIncludeCSSTransform = */ false);
6891 if (scale.xScale == 0 || scale.yScale == 0) {
6892 scale = MatrixScales();
6895 // Compute bounds for the scroll position, and computed the snapped scrolled
6896 // rect from the scroll position bounds.
6897 nscoord snappedScrolledAreaBottom =
6898 SnapCoord(scrolledRect.YMost(), scale.yScale, appUnitsPerDevPixel);
6899 nscoord snappedScrollPortBottom =
6900 SnapCoord(scrollPort.YMost(), scale.yScale, appUnitsPerDevPixel);
6901 nscoord maximumScrollOffsetY =
6902 snappedScrolledAreaBottom - snappedScrollPortBottom;
6903 result.SetBottomEdge(scrollPort.height + maximumScrollOffsetY);
6905 if (GetScrolledFrameDir() == StyleDirection::Ltr) {
6906 nscoord snappedScrolledAreaRight =
6907 SnapCoord(scrolledRect.XMost(), scale.xScale, appUnitsPerDevPixel);
6908 nscoord snappedScrollPortRight =
6909 SnapCoord(scrollPort.XMost(), scale.xScale, appUnitsPerDevPixel);
6910 nscoord maximumScrollOffsetX =
6911 snappedScrolledAreaRight - snappedScrollPortRight;
6912 result.SetRightEdge(scrollPort.width + maximumScrollOffsetX);
6913 } else {
6914 // In RTL, the scrolled area's right edge is at scrollPort.XMost(),
6915 // and the scrolled area's x position is zero or negative. We want
6916 // the right edge to stay flush with the scroll port, so we snap the
6917 // left edge.
6918 nscoord snappedScrolledAreaLeft =
6919 SnapCoord(scrolledRect.x, scale.xScale, appUnitsPerDevPixel);
6920 nscoord snappedScrollPortLeft =
6921 SnapCoord(scrollPort.x, scale.xScale, appUnitsPerDevPixel);
6922 nscoord minimumScrollOffsetX =
6923 snappedScrolledAreaLeft - snappedScrollPortLeft;
6924 result.SetLeftEdge(minimumScrollOffsetX);
6927 return result;
6930 StyleDirection nsHTMLScrollFrame::GetScrolledFrameDir() const {
6931 // If the scrolled frame has unicode-bidi: plaintext, the paragraph
6932 // direction set by the text content overrides the direction of the frame
6933 if (mScrolledFrame->StyleTextReset()->mUnicodeBidi ==
6934 StyleUnicodeBidi::Plaintext) {
6935 if (nsIFrame* child = mScrolledFrame->PrincipalChildList().FirstChild()) {
6936 return nsBidiPresUtils::ParagraphDirection(child) ==
6937 mozilla::intl::BidiDirection::LTR
6938 ? StyleDirection::Ltr
6939 : StyleDirection::Rtl;
6942 return IsBidiLTR() ? StyleDirection::Ltr : StyleDirection::Rtl;
6945 nsRect nsHTMLScrollFrame::GetUnsnappedScrolledRectInternal(
6946 const nsRect& aScrolledOverflowArea, const nsSize& aScrollPortSize) const {
6947 return nsLayoutUtils::GetScrolledRect(mScrolledFrame, aScrolledOverflowArea,
6948 aScrollPortSize, GetScrolledFrameDir());
6951 nsMargin nsHTMLScrollFrame::GetActualScrollbarSizes(
6952 nsIScrollableFrame::ScrollbarSizesOptions
6953 aOptions /* = nsIScrollableFrame::ScrollbarSizesOptions::NONE */)
6954 const {
6955 nsRect r = GetPaddingRectRelativeToSelf();
6957 nsMargin m(mScrollPort.y - r.y, r.XMost() - mScrollPort.XMost(),
6958 r.YMost() - mScrollPort.YMost(), mScrollPort.x - r.x);
6960 if (aOptions == nsIScrollableFrame::ScrollbarSizesOptions::
6961 INCLUDE_VISUAL_VIEWPORT_SCROLLBARS &&
6962 !UsesOverlayScrollbars()) {
6963 // If we are using layout scrollbars and they only exist to scroll the
6964 // visual viewport then they do not take up any layout space (so the
6965 // scrollport is the same as the padding rect) but they do cover everything
6966 // below them so some callers may want to include this special type of
6967 // scrollbars in the returned value.
6968 if (mHScrollbarBox && mHasHorizontalScrollbar &&
6969 mOnlyNeedHScrollbarToScrollVVInsideLV) {
6970 m.bottom += mHScrollbarBox->GetRect().height;
6972 if (mVScrollbarBox && mHasVerticalScrollbar &&
6973 mOnlyNeedVScrollbarToScrollVVInsideLV) {
6974 if (IsScrollbarOnRight()) {
6975 m.right += mVScrollbarBox->GetRect().width;
6976 } else {
6977 m.left += mVScrollbarBox->GetRect().width;
6982 return m;
6985 void nsHTMLScrollFrame::SetScrollbarVisibility(nsIFrame* aScrollbar,
6986 bool aVisible) {
6987 nsScrollbarFrame* scrollbar = do_QueryFrame(aScrollbar);
6988 if (scrollbar) {
6989 // See if we have a mediator.
6990 nsIScrollbarMediator* mediator = scrollbar->GetScrollbarMediator();
6991 if (mediator) {
6992 // Inform the mediator of the visibility change.
6993 mediator->VisibilityChanged(aVisible);
6998 nscoord nsHTMLScrollFrame::GetCoordAttribute(nsIFrame* aBox, nsAtom* aAtom,
6999 nscoord aDefaultValue,
7000 nscoord* aRangeStart,
7001 nscoord* aRangeLength) {
7002 if (aBox) {
7003 nsIContent* content = aBox->GetContent();
7005 nsAutoString value;
7006 if (content->IsElement()) {
7007 content->AsElement()->GetAttr(aAtom, value);
7009 if (!value.IsEmpty()) {
7010 nsresult error;
7011 // convert it to appunits
7012 nscoord result =
7013 nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error));
7014 nscoord halfPixel = nsPresContext::CSSPixelsToAppUnits(0.5f);
7015 // Any nscoord value that would round to the attribute value when
7016 // converted to CSS pixels is allowed.
7017 *aRangeStart = result - halfPixel;
7018 *aRangeLength = halfPixel * 2 - 1;
7019 return result;
7023 // Only this exact default value is allowed.
7024 *aRangeStart = aDefaultValue;
7025 *aRangeLength = 0;
7026 return aDefaultValue;
7029 bool nsHTMLScrollFrame::IsLastScrollUpdateAnimating() const {
7030 if (!mScrollUpdates.IsEmpty()) {
7031 switch (mScrollUpdates.LastElement().GetMode()) {
7032 case ScrollMode::Smooth:
7033 case ScrollMode::SmoothMsd:
7034 return true;
7035 case ScrollMode::Instant:
7036 case ScrollMode::Normal:
7037 break;
7040 return false;
7043 bool nsHTMLScrollFrame::IsLastScrollUpdateTriggeredByScriptAnimating() const {
7044 if (!mScrollUpdates.IsEmpty()) {
7045 const ScrollPositionUpdate& lastUpdate = mScrollUpdates.LastElement();
7046 if (lastUpdate.WasTriggeredByScript() &&
7047 (mScrollUpdates.LastElement().GetMode() == ScrollMode::Smooth ||
7048 mScrollUpdates.LastElement().GetMode() == ScrollMode::SmoothMsd)) {
7049 return true;
7052 return false;
7055 using AnimationState = nsIScrollableFrame::AnimationState;
7056 EnumSet<AnimationState> nsHTMLScrollFrame::ScrollAnimationState() const {
7057 EnumSet<AnimationState> retval;
7058 if (IsApzAnimationInProgress()) {
7059 retval += AnimationState::APZInProgress;
7060 if (mCurrentAPZScrollAnimationType ==
7061 APZScrollAnimationType::TriggeredByScript) {
7062 retval += AnimationState::TriggeredByScript;
7066 if (mApzAnimationRequested) {
7067 retval += AnimationState::APZRequested;
7068 if (mApzAnimationTriggeredByScriptRequested) {
7069 retval += AnimationState::TriggeredByScript;
7073 if (IsLastScrollUpdateAnimating()) {
7074 retval += AnimationState::APZPending;
7075 if (IsLastScrollUpdateTriggeredByScriptAnimating()) {
7076 retval += AnimationState::TriggeredByScript;
7079 if (mAsyncScroll) {
7080 retval += AnimationState::MainThread;
7081 if (mAsyncScroll->WasTriggeredByScript()) {
7082 retval += AnimationState::TriggeredByScript;
7086 if (mAsyncSmoothMSDScroll) {
7087 retval += AnimationState::MainThread;
7088 if (mAsyncSmoothMSDScroll->WasTriggeredByScript()) {
7089 retval += AnimationState::TriggeredByScript;
7092 return retval;
7095 void nsHTMLScrollFrame::ResetScrollInfoIfNeeded(
7096 const MainThreadScrollGeneration& aGeneration,
7097 const APZScrollGeneration& aGenerationOnApz,
7098 APZScrollAnimationType aAPZScrollAnimationType,
7099 InScrollingGesture aInScrollingGesture) {
7100 if (aGeneration == mScrollGeneration) {
7101 mLastScrollOrigin = ScrollOrigin::None;
7102 mApzAnimationRequested = false;
7103 mApzAnimationTriggeredByScriptRequested = false;
7106 mScrollGenerationOnApz = aGenerationOnApz;
7107 // We can reset this regardless of scroll generation, as this is only set
7108 // here, as a response to APZ requesting a repaint.
7109 mCurrentAPZScrollAnimationType = aAPZScrollAnimationType;
7111 mInScrollingGesture = aInScrollingGesture;
7114 UniquePtr<PresState> nsHTMLScrollFrame::SaveState() {
7115 nsIScrollbarMediator* mediator = do_QueryFrame(GetScrolledFrame());
7116 if (mediator) {
7117 // child handles its own scroll state, so don't bother saving state here
7118 return nullptr;
7121 // Don't store a scroll state if we never have been scrolled or restored
7122 // a previous scroll state, and we're not in the middle of a smooth scroll.
7123 auto scrollAnimationState = ScrollAnimationState();
7124 bool isScrollAnimating =
7125 scrollAnimationState.contains(AnimationState::MainThread) ||
7126 scrollAnimationState.contains(AnimationState::APZPending) ||
7127 scrollAnimationState.contains(AnimationState::APZRequested);
7128 if (!mHasBeenScrolled && !mDidHistoryRestore && !isScrollAnimating) {
7129 return nullptr;
7132 UniquePtr<PresState> state = NewPresState();
7133 bool allowScrollOriginDowngrade =
7134 !nsLayoutUtils::CanScrollOriginClobberApz(mLastScrollOrigin) ||
7135 mAllowScrollOriginDowngrade;
7136 // Save mRestorePos instead of our actual current scroll position, if it's
7137 // valid and we haven't moved since the last update of mLastPos (same check
7138 // that ScrollToRestoredPosition uses). This ensures if a reframe occurs
7139 // while we're in the process of loading content to scroll to a restored
7140 // position, we'll keep trying after the reframe. Similarly, if we're in the
7141 // middle of a smooth scroll, store the destination so that when we restore
7142 // we'll jump straight to the end of the scroll animation, rather than
7143 // effectively dropping it. Note that the mRestorePos will override the
7144 // smooth scroll destination if both are present.
7145 nsPoint pt = GetLogicalVisualViewportOffset();
7146 if (isScrollAnimating) {
7147 pt = mDestination;
7148 allowScrollOriginDowngrade = false;
7150 SCROLLRESTORE_LOG("%p: SaveState, pt=%s, mLastPos=%s, mRestorePos=%s\n", this,
7151 ToString(pt).c_str(), ToString(mLastPos).c_str(),
7152 ToString(mRestorePos).c_str());
7153 if (mRestorePos.y != -1 && pt == mLastPos) {
7154 pt = mRestorePos;
7156 state->scrollState() = pt;
7157 state->allowScrollOriginDowngrade() = allowScrollOriginDowngrade;
7158 if (mIsRoot) {
7159 // Only save resolution properties for root scroll frames
7160 state->resolution() = PresShell()->GetResolution();
7162 return state;
7165 NS_IMETHODIMP nsHTMLScrollFrame::RestoreState(PresState* aState) {
7166 mRestorePos = aState->scrollState();
7167 MOZ_ASSERT(mLastScrollOrigin == ScrollOrigin::None);
7168 mAllowScrollOriginDowngrade = aState->allowScrollOriginDowngrade();
7169 // When restoring state, we promote mLastScrollOrigin to a stronger value
7170 // from the default of eNone, to restore the behaviour that existed when
7171 // the state was saved. If mLastScrollOrigin was a weaker value previously,
7172 // then mAllowScrollOriginDowngrade will be true, and so the combination of
7173 // mAllowScrollOriginDowngrade and the stronger mLastScrollOrigin will allow
7174 // the same types of scrolls as before. It might be possible to also just
7175 // save and restore the mAllowScrollOriginDowngrade and mLastScrollOrigin
7176 // values directly without this sort of fiddling. Something to try in the
7177 // future or if we tinker with this code more.
7178 mLastScrollOrigin = ScrollOrigin::Other;
7179 mDidHistoryRestore = true;
7180 mLastPos = mScrolledFrame ? GetLogicalVisualViewportOffset() : nsPoint(0, 0);
7181 SCROLLRESTORE_LOG("%p: RestoreState, set mRestorePos=%s mLastPos=%s\n", this,
7182 ToString(mRestorePos).c_str(), ToString(mLastPos).c_str());
7184 // Resolution properties should only exist on root scroll frames.
7185 MOZ_ASSERT(mIsRoot || aState->resolution() == 1.0);
7187 if (mIsRoot) {
7188 PresShell()->SetResolutionAndScaleTo(
7189 aState->resolution(), ResolutionChangeOrigin::MainThreadRestore);
7191 return NS_OK;
7194 void nsHTMLScrollFrame::PostScrolledAreaEvent() {
7195 if (mScrolledAreaEvent.IsPending()) {
7196 return;
7198 mScrolledAreaEvent = new ScrolledAreaEvent(this);
7199 nsContentUtils::AddScriptRunner(mScrolledAreaEvent.get());
7202 ////////////////////////////////////////////////////////////////////////////////
7203 // ScrolledArea change event dispatch
7205 // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
7206 MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
7207 nsHTMLScrollFrame::ScrolledAreaEvent::Run() {
7208 if (mHelper) {
7209 mHelper->FireScrolledAreaEvent();
7211 return NS_OK;
7214 void nsHTMLScrollFrame::FireScrolledAreaEvent() {
7215 mScrolledAreaEvent.Forget();
7217 InternalScrollAreaEvent event(true, eScrolledAreaChanged, nullptr);
7218 RefPtr<nsPresContext> presContext = PresContext();
7219 nsIContent* content = GetContent();
7221 event.mArea = mScrolledFrame->ScrollableOverflowRectRelativeToParent();
7222 if (RefPtr<Document> doc = content->GetUncomposedDoc()) {
7223 EventDispatcher::Dispatch(doc, presContext, &event, nullptr);
7227 ScrollDirections nsIScrollableFrame::GetAvailableScrollingDirections() const {
7228 nscoord oneDevPixel =
7229 GetScrolledFrame()->PresContext()->AppUnitsPerDevPixel();
7230 ScrollDirections directions;
7231 nsRect scrollRange = GetScrollRange();
7232 if (scrollRange.width >= oneDevPixel) {
7233 directions += ScrollDirection::eHorizontal;
7235 if (scrollRange.height >= oneDevPixel) {
7236 directions += ScrollDirection::eVertical;
7238 return directions;
7241 nsRect nsHTMLScrollFrame::GetScrollRangeForUserInputEvents() const {
7242 // This function computes a scroll range based on a scrolled rect and scroll
7243 // port defined as follows:
7244 // scrollable rect = overflow:hidden ? layout viewport : scrollable rect
7245 // scroll port = have visual viewport ? visual viewport : layout viewport
7246 // The results in the same notion of scroll range that APZ uses (the combined
7247 // effect of FrameMetrics::CalculateScrollRange() and
7248 // nsLayoutUtils::CalculateScrollableRectForFrame).
7250 ScrollStyles ss = GetScrollStyles();
7252 nsPoint scrollPos = GetScrollPosition();
7254 nsRect scrolledRect = GetScrolledRect();
7255 if (StyleOverflow::Hidden == ss.mHorizontal) {
7256 scrolledRect.width = mScrollPort.width;
7257 scrolledRect.x = scrollPos.x;
7259 if (StyleOverflow::Hidden == ss.mVertical) {
7260 scrolledRect.height = mScrollPort.height;
7261 scrolledRect.y = scrollPos.y;
7264 nsSize scrollPort = GetVisualViewportSize();
7266 nsRect scrollRange = scrolledRect;
7267 scrollRange.width = std::max(scrolledRect.width - scrollPort.width, 0);
7268 scrollRange.height = std::max(scrolledRect.height - scrollPort.height, 0);
7270 return scrollRange;
7273 ScrollDirections
7274 nsHTMLScrollFrame::GetAvailableScrollingDirectionsForUserInputEvents() const {
7275 nsRect scrollRange = GetScrollRangeForUserInputEvents();
7277 // We check if there is at least one half of a screen pixel of scroll range to
7278 // roughly match what apz does when it checks if the change in scroll position
7279 // in screen pixels round to zero or not.
7280 // (https://searchfox.org/mozilla-central/rev/2f09184ec781a2667feec87499d4b81b32b6c48e/gfx/layers/apz/src/AsyncPanZoomController.cpp#3210)
7281 // This isn't quite half a screen pixel, it doesn't take into account CSS
7282 // transforms, but should be good enough.
7283 float halfScreenPixel =
7284 GetScrolledFrame()->PresContext()->AppUnitsPerDevPixel() /
7285 (PresShell()->GetCumulativeResolution() * 2.f);
7286 ScrollDirections directions;
7287 if (scrollRange.width >= halfScreenPixel) {
7288 directions += ScrollDirection::eHorizontal;
7290 if (scrollRange.height >= halfScreenPixel) {
7291 directions += ScrollDirection::eVertical;
7293 return directions;
7297 * Append scroll positions for valid snap positions into |aSnapInfo| if
7298 * applicable.
7300 static void AppendScrollPositionsForSnap(
7301 const nsIFrame* aFrame, const nsIFrame* aScrolledFrame,
7302 const nsRect& aScrolledRect, const nsMargin& aScrollPadding,
7303 const nsRect& aScrollRange, WritingMode aWritingModeOnScroller,
7304 ScrollSnapInfo& aSnapInfo, nsHTMLScrollFrame::SnapTargetSet* aSnapTargets) {
7305 ScrollSnapTargetId targetId = ScrollSnapUtils::GetTargetIdFor(aFrame);
7307 nsRect snapArea =
7308 ScrollSnapUtils::GetSnapAreaFor(aFrame, aScrolledFrame, aScrolledRect);
7309 // Use the writing-mode on the target element if the snap area is larger than
7310 // the snapport.
7311 // https://drafts.csswg.org/css-scroll-snap/#snap-scope
7312 WritingMode writingMode = ScrollSnapUtils::NeedsToRespectTargetWritingMode(
7313 snapArea.Size(), aSnapInfo.mSnapportSize)
7314 ? aFrame->GetWritingMode()
7315 : aWritingModeOnScroller;
7317 // These snap range shouldn't be involved with scroll-margin since we just
7318 // need the visible range of the target element.
7319 if (snapArea.width > aSnapInfo.mSnapportSize.width) {
7320 aSnapInfo.mXRangeWiderThanSnapport.AppendElement(
7321 ScrollSnapInfo::ScrollSnapRange(snapArea, ScrollDirection::eHorizontal,
7322 targetId));
7324 if (snapArea.height > aSnapInfo.mSnapportSize.height) {
7325 aSnapInfo.mYRangeWiderThanSnapport.AppendElement(
7326 ScrollSnapInfo::ScrollSnapRange(snapArea, ScrollDirection::eVertical,
7327 targetId));
7330 // Shift target rect position by the scroll padding to get the padded
7331 // position thus we don't need to take account scroll-padding values in
7332 // ScrollSnapUtils::GetSnapPointForDestination() when it gets called from
7333 // the compositor thread.
7334 snapArea.y -= aScrollPadding.top;
7335 snapArea.x -= aScrollPadding.left;
7337 LogicalRect logicalTargetRect(writingMode, snapArea, aSnapInfo.mSnapportSize);
7338 LogicalSize logicalSnapportRect(writingMode, aSnapInfo.mSnapportSize);
7339 LogicalRect logicalScrollRange(aWritingModeOnScroller, aScrollRange,
7340 // The origin of this logical coordinate system
7341 // what we need here is (0, 0), so we use an
7342 // empty size.
7343 nsSize());
7345 Maybe<nscoord> blockDirectionPosition;
7346 const nsStyleDisplay* styleDisplay = aFrame->StyleDisplay();
7347 nscoord containerBSize = logicalSnapportRect.BSize(writingMode);
7348 switch (styleDisplay->mScrollSnapAlign.block) {
7349 case StyleScrollSnapAlignKeyword::None:
7350 break;
7351 case StyleScrollSnapAlignKeyword::Start:
7352 blockDirectionPosition.emplace(
7353 writingMode.IsVerticalRL() ? -logicalTargetRect.BStart(writingMode)
7354 : logicalTargetRect.BStart(writingMode));
7355 break;
7356 case StyleScrollSnapAlignKeyword::End: {
7357 nscoord candidate = std::clamp(
7358 // What we need here is the scroll position instead of the snap
7359 // position itself, so we need, for example, the top edge of the
7360 // scroll port on horizontal-tb when the frame is positioned at
7361 // the bottom edge of the scroll port. For this reason we subtract
7362 // containerBSize from BEnd of the target and clamp it inside the
7363 // scrollable range.
7364 logicalTargetRect.BEnd(writingMode) - containerBSize,
7365 logicalScrollRange.BStart(writingMode),
7366 logicalScrollRange.BEnd(writingMode));
7367 blockDirectionPosition.emplace(writingMode.IsVerticalRL() ? -candidate
7368 : candidate);
7369 break;
7371 case StyleScrollSnapAlignKeyword::Center: {
7372 nscoord targetCenter = (logicalTargetRect.BStart(writingMode) +
7373 logicalTargetRect.BEnd(writingMode)) /
7375 nscoord halfSnapportSize = containerBSize / 2;
7376 // Get the center of the target to align with the center of the snapport
7377 // depending on direction.
7378 nscoord candidate = std::clamp(targetCenter - halfSnapportSize,
7379 logicalScrollRange.BStart(writingMode),
7380 logicalScrollRange.BEnd(writingMode));
7381 blockDirectionPosition.emplace(writingMode.IsVerticalRL() ? -candidate
7382 : candidate);
7383 break;
7387 Maybe<nscoord> inlineDirectionPosition;
7388 nscoord containerISize = logicalSnapportRect.ISize(writingMode);
7389 switch (styleDisplay->mScrollSnapAlign.inline_) {
7390 case StyleScrollSnapAlignKeyword::None:
7391 break;
7392 case StyleScrollSnapAlignKeyword::Start:
7393 inlineDirectionPosition.emplace(
7394 writingMode.IsInlineReversed()
7395 ? -logicalTargetRect.IStart(writingMode)
7396 : logicalTargetRect.IStart(writingMode));
7397 break;
7398 case StyleScrollSnapAlignKeyword::End: {
7399 nscoord candidate = std::clamp(
7400 // Same as above BEnd case, we subtract containerISize.
7402 // Note that the logical scroll range is mapped to [0, x] range even
7403 // if it's in RTL contents. So for example, if the physical range is
7404 // [-200, 0], it's mapped to [0, 200], i.e. IStart() is 0, IEnd() is
7405 // 200. So we can just use std::clamp with the same arguments in both
7406 // RTL/LTR cases.
7407 logicalTargetRect.IEnd(writingMode) - containerISize,
7408 logicalScrollRange.IStart(writingMode),
7409 logicalScrollRange.IEnd(writingMode));
7410 inlineDirectionPosition.emplace(
7411 writingMode.IsInlineReversed() ? -candidate : candidate);
7412 break;
7414 case StyleScrollSnapAlignKeyword::Center: {
7415 nscoord targetCenter = (logicalTargetRect.IStart(writingMode) +
7416 logicalTargetRect.IEnd(writingMode)) /
7418 nscoord halfSnapportSize = containerISize / 2;
7419 // Get the center of the target to align with the center of the snapport
7420 // depending on direction.
7421 nscoord candidate = std::clamp(targetCenter - halfSnapportSize,
7422 logicalScrollRange.IStart(writingMode),
7423 logicalScrollRange.IEnd(writingMode));
7424 inlineDirectionPosition.emplace(
7425 writingMode.IsInlineReversed() ? -candidate : candidate);
7426 break;
7430 if (blockDirectionPosition || inlineDirectionPosition) {
7431 aSnapInfo.mSnapTargets.AppendElement(
7432 writingMode.IsVertical()
7433 ? ScrollSnapInfo::SnapTarget(
7434 std::move(blockDirectionPosition),
7435 std::move(inlineDirectionPosition), std::move(snapArea),
7436 styleDisplay->mScrollSnapStop, targetId)
7437 : ScrollSnapInfo::SnapTarget(
7438 std::move(inlineDirectionPosition),
7439 std::move(blockDirectionPosition), std::move(snapArea),
7440 styleDisplay->mScrollSnapStop, targetId));
7441 if (aSnapTargets) {
7442 aSnapTargets->EnsureInserted(aFrame->GetContent());
7448 * Collect the scroll positions corresponding to snap positions of frames in the
7449 * subtree rooted at |aFrame|, relative to |aScrolledFrame|, into |aSnapInfo|.
7451 static void CollectScrollPositionsForSnap(
7452 nsIFrame* aFrame, nsIFrame* aScrolledFrame, const nsRect& aScrolledRect,
7453 const nsMargin& aScrollPadding, const nsRect& aScrollRange,
7454 WritingMode aWritingModeOnScroller, ScrollSnapInfo& aSnapInfo,
7455 nsHTMLScrollFrame::SnapTargetSet* aSnapTargets) {
7456 // Snap positions only affect the nearest ancestor scroll container on the
7457 // element's containing block chain.
7458 nsIScrollableFrame* sf = do_QueryFrame(aFrame);
7459 if (sf) {
7460 return;
7463 for (const auto& childList : aFrame->ChildLists()) {
7464 for (nsIFrame* f : childList.mList) {
7465 const nsStyleDisplay* styleDisplay = f->StyleDisplay();
7466 if (styleDisplay->mScrollSnapAlign.inline_ !=
7467 StyleScrollSnapAlignKeyword::None ||
7468 styleDisplay->mScrollSnapAlign.block !=
7469 StyleScrollSnapAlignKeyword::None) {
7470 AppendScrollPositionsForSnap(
7471 f, aScrolledFrame, aScrolledRect, aScrollPadding, aScrollRange,
7472 aWritingModeOnScroller, aSnapInfo, aSnapTargets);
7474 CollectScrollPositionsForSnap(
7475 f, aScrolledFrame, aScrolledRect, aScrollPadding, aScrollRange,
7476 aWritingModeOnScroller, aSnapInfo, aSnapTargets);
7481 static nscoord ResolveScrollPaddingStyleValue(
7482 const StyleRect<mozilla::NonNegativeLengthPercentageOrAuto>&
7483 aScrollPaddingStyle,
7484 Side aSide, const nsSize& aScrollPortSize) {
7485 if (aScrollPaddingStyle.Get(aSide).IsAuto()) {
7486 // https://drafts.csswg.org/css-scroll-snap-1/#valdef-scroll-padding-auto
7487 return 0;
7490 nscoord percentageBasis;
7491 switch (aSide) {
7492 case eSideTop:
7493 case eSideBottom:
7494 percentageBasis = aScrollPortSize.height;
7495 break;
7496 case eSideLeft:
7497 case eSideRight:
7498 percentageBasis = aScrollPortSize.width;
7499 break;
7502 return aScrollPaddingStyle.Get(aSide).AsLengthPercentage().Resolve(
7503 percentageBasis);
7506 static nsMargin ResolveScrollPaddingStyle(
7507 const StyleRect<mozilla::NonNegativeLengthPercentageOrAuto>&
7508 aScrollPaddingStyle,
7509 const nsSize& aScrollPortSize) {
7510 return nsMargin(ResolveScrollPaddingStyleValue(aScrollPaddingStyle, eSideTop,
7511 aScrollPortSize),
7512 ResolveScrollPaddingStyleValue(aScrollPaddingStyle,
7513 eSideRight, aScrollPortSize),
7514 ResolveScrollPaddingStyleValue(aScrollPaddingStyle,
7515 eSideBottom, aScrollPortSize),
7516 ResolveScrollPaddingStyleValue(aScrollPaddingStyle, eSideLeft,
7517 aScrollPortSize));
7520 nsMargin nsHTMLScrollFrame::GetScrollPadding() const {
7521 nsIFrame* styleFrame = GetFrameForStyle();
7522 if (!styleFrame) {
7523 return nsMargin();
7526 // The spec says percentage values are relative to the scroll port size.
7527 // https://drafts.csswg.org/css-scroll-snap-1/#scroll-padding
7528 return ResolveScrollPaddingStyle(styleFrame->StylePadding()->mScrollPadding,
7529 GetScrollPortRect().Size());
7532 ScrollSnapInfo nsHTMLScrollFrame::ComputeScrollSnapInfo() {
7533 ScrollSnapInfo result;
7535 nsIFrame* scrollSnapFrame = GetFrameForStyle();
7536 if (!scrollSnapFrame) {
7537 return result;
7540 const nsStyleDisplay* disp = scrollSnapFrame->StyleDisplay();
7541 if (disp->mScrollSnapType.strictness == StyleScrollSnapStrictness::None) {
7542 // We won't be snapping, short-circuit the computation.
7543 return result;
7546 WritingMode writingMode = GetWritingMode();
7547 result.InitializeScrollSnapStrictness(writingMode, disp);
7549 result.mSnapportSize = GetSnapportSize();
7550 CollectScrollPositionsForSnap(
7551 mScrolledFrame, mScrolledFrame, GetScrolledRect(), GetScrollPadding(),
7552 GetLayoutScrollRange(), writingMode, result, &mSnapTargets);
7553 return result;
7556 ScrollSnapInfo nsHTMLScrollFrame::GetScrollSnapInfo() {
7557 // TODO(botond): Should we cache it?
7558 return ComputeScrollSnapInfo();
7561 Maybe<SnapDestination> nsHTMLScrollFrame::GetSnapPointForDestination(
7562 ScrollUnit aUnit, ScrollSnapFlags aFlags, const nsPoint& aStartPos,
7563 const nsPoint& aDestination) {
7564 // We can release the strong references for the previous snap target
7565 // elements here since calling this ComputeScrollSnapInfo means we are going
7566 // to evaluate new snap points, thus there's no chance to generating
7567 // nsIContent instances in between this function call and the function call
7568 // for the (re-)evaluation.
7569 mSnapTargets.Clear();
7570 return ScrollSnapUtils::GetSnapPointForDestination(
7571 ComputeScrollSnapInfo(), aUnit, aFlags, GetLayoutScrollRange(), aStartPos,
7572 aDestination);
7575 Maybe<SnapDestination> nsHTMLScrollFrame::GetSnapPointForResnap() {
7576 // Same as in GetSnapPointForDestination, We can release the strong references
7577 // for the previous snap targets here.
7578 mSnapTargets.Clear();
7579 nsIContent* focusedContent =
7580 GetContent()->GetComposedDoc()->GetUnretargetedFocusedContent();
7581 return ScrollSnapUtils::GetSnapPointForResnap(
7582 ComputeScrollSnapInfo(), GetLayoutScrollRange(), GetScrollPosition(),
7583 mLastSnapTargetIds, focusedContent);
7586 bool nsHTMLScrollFrame::NeedsResnap() {
7587 nsIContent* focusedContent =
7588 GetContent()->GetComposedDoc()->GetUnretargetedFocusedContent();
7589 return ScrollSnapUtils::GetSnapPointForResnap(
7590 ComputeScrollSnapInfo(), GetLayoutScrollRange(),
7591 GetScrollPosition(), mLastSnapTargetIds, focusedContent)
7592 .isSome();
7595 void nsHTMLScrollFrame::SetLastSnapTargetIds(
7596 UniquePtr<ScrollSnapTargetIds> aIds) {
7597 if (!aIds) {
7598 mLastSnapTargetIds = nullptr;
7599 return;
7602 // This SetLastSnapTargetIds gets called asynchronously so that there's a race
7603 // condition something like;
7604 // 1) an async scroll operation triggered snapping to a point on an element
7605 // 2) during the async scroll operation, the element got removed from this
7606 // scroll container
7607 // 3) re-snapping triggered
7608 // 4) this SetLastSnapTargetIds got called
7609 // In such case |aIds| are stale, we shouldn't use it.
7610 for (const auto* idList : {&aIds->mIdsOnX, &aIds->mIdsOnY}) {
7611 for (const auto id : *idList) {
7612 if (!mSnapTargets.Contains(reinterpret_cast<nsIContent*>(id))) {
7613 mLastSnapTargetIds = nullptr;
7614 return;
7619 mLastSnapTargetIds = std::move(aIds);
7622 bool nsHTMLScrollFrame::IsLastSnappedTarget(const nsIFrame* aFrame) const {
7623 ScrollSnapTargetId id = ScrollSnapUtils::GetTargetIdFor(aFrame);
7624 MOZ_ASSERT(id != ScrollSnapTargetId::None,
7625 "This function is supposed to be called for contents");
7627 if (!mLastSnapTargetIds) {
7628 return false;
7631 return mLastSnapTargetIds->mIdsOnX.Contains(id) ||
7632 mLastSnapTargetIds->mIdsOnY.Contains(id);
7635 void nsHTMLScrollFrame::TryResnap() {
7636 // If there's any async scroll is running or we are processing pan/touch
7637 // gestures or scroll thumb dragging, don't clobber the scroll.
7638 if (!ScrollAnimationState().isEmpty() ||
7639 mInScrollingGesture == InScrollingGesture::Yes) {
7640 return;
7643 if (auto snapDestination = GetSnapPointForResnap()) {
7644 // We are going to re-snap so that we need to clobber scroll anchoring.
7645 mAnchor.UserScrolled();
7647 // Snap to the nearest snap position if exists.
7648 ScrollToWithOrigin(
7649 snapDestination->mPosition, nullptr /* range */,
7650 ScrollOperationParams{
7651 IsSmoothScroll(ScrollBehavior::Auto) ? ScrollMode::SmoothMsd
7652 : ScrollMode::Instant,
7653 ScrollOrigin::Other, std::move(snapDestination->mTargetIds)});
7657 void nsHTMLScrollFrame::PostPendingResnapIfNeeded(const nsIFrame* aFrame) {
7658 if (!IsLastSnappedTarget(aFrame)) {
7659 return;
7662 PostPendingResnap();
7665 void nsHTMLScrollFrame::PostPendingResnap() {
7666 PresShell()->PostPendingScrollResnap(this);
7669 nsIScrollableFrame::PhysicalScrollSnapAlign
7670 nsHTMLScrollFrame::GetScrollSnapAlignFor(const nsIFrame* aFrame) const {
7671 StyleScrollSnapAlignKeyword alignForY = StyleScrollSnapAlignKeyword::None;
7672 StyleScrollSnapAlignKeyword alignForX = StyleScrollSnapAlignKeyword::None;
7674 nsIFrame* styleFrame = GetFrameForStyle();
7675 if (!styleFrame) {
7676 return {alignForX, alignForY};
7679 if (styleFrame->StyleDisplay()->mScrollSnapType.strictness ==
7680 StyleScrollSnapStrictness::None) {
7681 return {alignForX, alignForY};
7684 const nsStyleDisplay* styleDisplay = aFrame->StyleDisplay();
7685 if (styleDisplay->mScrollSnapAlign.inline_ ==
7686 StyleScrollSnapAlignKeyword::None &&
7687 styleDisplay->mScrollSnapAlign.block ==
7688 StyleScrollSnapAlignKeyword::None) {
7689 return {alignForX, alignForY};
7692 nsSize snapAreaSize =
7693 ScrollSnapUtils::GetSnapAreaFor(aFrame, mScrolledFrame, GetScrolledRect())
7694 .Size();
7695 const WritingMode writingMode =
7696 ScrollSnapUtils::NeedsToRespectTargetWritingMode(snapAreaSize,
7697 GetSnapportSize())
7698 ? aFrame->GetWritingMode()
7699 : styleFrame->GetWritingMode();
7701 switch (styleFrame->StyleDisplay()->mScrollSnapType.axis) {
7702 case StyleScrollSnapAxis::X:
7703 alignForX = writingMode.IsVertical()
7704 ? styleDisplay->mScrollSnapAlign.block
7705 : styleDisplay->mScrollSnapAlign.inline_;
7706 break;
7707 case StyleScrollSnapAxis::Y:
7708 alignForY = writingMode.IsVertical()
7709 ? styleDisplay->mScrollSnapAlign.inline_
7710 : styleDisplay->mScrollSnapAlign.block;
7711 break;
7712 case StyleScrollSnapAxis::Block:
7713 if (writingMode.IsVertical()) {
7714 alignForX = styleDisplay->mScrollSnapAlign.block;
7715 } else {
7716 alignForY = styleDisplay->mScrollSnapAlign.block;
7718 break;
7719 case StyleScrollSnapAxis::Inline:
7720 if (writingMode.IsVertical()) {
7721 alignForY = styleDisplay->mScrollSnapAlign.inline_;
7722 } else {
7723 alignForX = styleDisplay->mScrollSnapAlign.inline_;
7725 break;
7726 case StyleScrollSnapAxis::Both:
7727 if (writingMode.IsVertical()) {
7728 alignForX = styleDisplay->mScrollSnapAlign.block;
7729 alignForY = styleDisplay->mScrollSnapAlign.inline_;
7730 } else {
7731 alignForX = styleDisplay->mScrollSnapAlign.inline_;
7732 alignForY = styleDisplay->mScrollSnapAlign.block;
7734 break;
7737 return {alignForX, alignForY};
7740 bool nsHTMLScrollFrame::UsesOverlayScrollbars() const {
7741 return PresContext()->UseOverlayScrollbars();
7744 bool nsHTMLScrollFrame::DragScroll(WidgetEvent* aEvent) {
7745 // Dragging is allowed while within a 20 pixel border. Note that device pixels
7746 // are used so that the same margin is used even when zoomed in or out.
7747 nscoord margin = 20 * PresContext()->AppUnitsPerDevPixel();
7749 // Don't drag scroll for small scrollareas.
7750 if (mScrollPort.width < margin * 2 || mScrollPort.height < margin * 2) {
7751 return false;
7754 // If willScroll is computed as false, then the frame is already scrolled as
7755 // far as it can go in both directions. Return false so that an ancestor
7756 // scrollframe can scroll instead.
7757 bool willScroll = false;
7758 nsPoint pnt =
7759 nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, RelativeTo{this});
7760 nsPoint scrollPoint = GetScrollPosition();
7761 nsRect rangeRect = GetLayoutScrollRange();
7763 // Only drag scroll when a scrollbar is present.
7764 nsPoint offset;
7765 if (mHasHorizontalScrollbar) {
7766 if (pnt.x >= mScrollPort.x && pnt.x <= mScrollPort.x + margin) {
7767 offset.x = -margin;
7768 if (scrollPoint.x > 0) {
7769 willScroll = true;
7771 } else if (pnt.x >= mScrollPort.XMost() - margin &&
7772 pnt.x <= mScrollPort.XMost()) {
7773 offset.x = margin;
7774 if (scrollPoint.x < rangeRect.width) {
7775 willScroll = true;
7780 if (mHasVerticalScrollbar) {
7781 if (pnt.y >= mScrollPort.y && pnt.y <= mScrollPort.y + margin) {
7782 offset.y = -margin;
7783 if (scrollPoint.y > 0) {
7784 willScroll = true;
7786 } else if (pnt.y >= mScrollPort.YMost() - margin &&
7787 pnt.y <= mScrollPort.YMost()) {
7788 offset.y = margin;
7789 if (scrollPoint.y < rangeRect.height) {
7790 willScroll = true;
7795 if (offset.x || offset.y) {
7796 ScrollToWithOrigin(
7797 GetScrollPosition() + offset, nullptr /* range */,
7798 ScrollOperationParams{ScrollMode::Normal, ScrollOrigin::Other});
7801 return willScroll;
7804 static nsSliderFrame* GetSliderFrame(nsIFrame* aScrollbarFrame) {
7805 if (!aScrollbarFrame) {
7806 return nullptr;
7809 for (const auto& childList : aScrollbarFrame->ChildLists()) {
7810 for (nsIFrame* frame : childList.mList) {
7811 if (nsSliderFrame* sliderFrame = do_QueryFrame(frame)) {
7812 return sliderFrame;
7816 return nullptr;
7819 static void AsyncScrollbarDragInitiated(uint64_t aDragBlockId,
7820 nsIFrame* aScrollbar) {
7821 if (nsSliderFrame* sliderFrame = GetSliderFrame(aScrollbar)) {
7822 sliderFrame->AsyncScrollbarDragInitiated(aDragBlockId);
7826 void nsHTMLScrollFrame::AsyncScrollbarDragInitiated(
7827 uint64_t aDragBlockId, ScrollDirection aDirection) {
7828 switch (aDirection) {
7829 case ScrollDirection::eVertical:
7830 ::AsyncScrollbarDragInitiated(aDragBlockId, mVScrollbarBox);
7831 break;
7832 case ScrollDirection::eHorizontal:
7833 ::AsyncScrollbarDragInitiated(aDragBlockId, mHScrollbarBox);
7834 break;
7838 static void AsyncScrollbarDragRejected(nsIFrame* aScrollbar) {
7839 if (nsSliderFrame* sliderFrame = GetSliderFrame(aScrollbar)) {
7840 sliderFrame->AsyncScrollbarDragRejected();
7844 void nsHTMLScrollFrame::AsyncScrollbarDragRejected() {
7845 // We don't get told which scrollbar requested the async drag,
7846 // so we notify both.
7847 ::AsyncScrollbarDragRejected(mHScrollbarBox);
7848 ::AsyncScrollbarDragRejected(mVScrollbarBox);
7851 void nsHTMLScrollFrame::ApzSmoothScrollTo(
7852 const nsPoint& aDestination, ScrollMode aMode, ScrollOrigin aOrigin,
7853 ScrollTriggeredByScript aTriggeredByScript,
7854 UniquePtr<ScrollSnapTargetIds> aSnapTargetIds) {
7855 if (mApzSmoothScrollDestination == Some(aDestination)) {
7856 // If we already sent APZ a smooth-scroll request to this
7857 // destination (i.e. it was the last request
7858 // we sent), then don't send another one because it is redundant.
7859 // This is to avoid a scenario where pages do repeated scrollBy
7860 // calls, incrementing the generation counter, and blocking APZ from
7861 // syncing the scroll offset back to the main thread.
7862 // Note that if we get two smooth-scroll requests to the same
7863 // destination with some other scroll in between,
7864 // mApzSmoothScrollDestination will get reset to Nothing() and so
7865 // we shouldn't have the problem where this check discards a
7866 // legitimate smooth-scroll.
7867 return;
7870 // The animation will be handled in the compositor, pass the
7871 // information needed to start the animation and skip the main-thread
7872 // animation for this scroll.
7873 MOZ_ASSERT(aOrigin != ScrollOrigin::None);
7874 mApzSmoothScrollDestination = Some(aDestination);
7875 AppendScrollUpdate(ScrollPositionUpdate::NewSmoothScroll(
7876 aMode, aOrigin, aDestination, aTriggeredByScript,
7877 std::move(aSnapTargetIds)));
7879 nsIContent* content = GetContent();
7880 if (!DisplayPortUtils::HasNonMinimalNonZeroDisplayPort(content)) {
7881 // If this frame doesn't have a displayport then there won't be an
7882 // APZC instance for it and so there won't be anything to process
7883 // this smooth scroll request. We should set a displayport on this
7884 // frame to force an APZC which can handle the request.
7885 if (MOZ_LOG_TEST(sDisplayportLog, LogLevel::Debug)) {
7886 mozilla::layers::ScrollableLayerGuid::ViewID viewID =
7887 mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID;
7888 nsLayoutUtils::FindIDFor(content, &viewID);
7889 MOZ_LOG(
7890 sDisplayportLog, LogLevel::Debug,
7891 ("ApzSmoothScrollTo setting displayport on scrollId=%" PRIu64 "\n",
7892 viewID));
7895 DisplayPortUtils::CalculateAndSetDisplayPortMargins(
7896 GetScrollTargetFrame(), DisplayPortUtils::RepaintMode::Repaint);
7897 nsIFrame* frame = do_QueryFrame(GetScrollTargetFrame());
7898 DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(frame);
7901 // Schedule a paint to ensure that the frame metrics get updated on
7902 // the compositor thread.
7903 SchedulePaint();
7906 bool nsHTMLScrollFrame::CanApzScrollInTheseDirections(
7907 ScrollDirections aDirections) {
7908 ScrollStyles styles = GetScrollStyles();
7909 if (aDirections.contains(ScrollDirection::eHorizontal) &&
7910 styles.mHorizontal == StyleOverflow::Hidden)
7911 return false;
7912 if (aDirections.contains(ScrollDirection::eVertical) &&
7913 styles.mVertical == StyleOverflow::Hidden)
7914 return false;
7915 return true;
7918 bool nsHTMLScrollFrame::SmoothScrollVisual(
7919 const nsPoint& aVisualViewportOffset,
7920 FrameMetrics::ScrollOffsetUpdateType aUpdateType) {
7921 bool canDoApzSmoothScroll =
7922 nsLayoutUtils::AsyncPanZoomEnabled(this) && WantAsyncScroll();
7923 if (!canDoApzSmoothScroll) {
7924 return false;
7927 // Clamp the destination to the visual scroll range.
7928 // There is a potential issue here, where |mDestination| is usually
7929 // clamped to the layout scroll range, and so e.g. a subsequent
7930 // window.scrollBy() may have an undesired effect. However, as this function
7931 // is only called internally, this should not be a problem in practice.
7932 // If it turns out to be, the fix would be:
7933 // - add a new "destination" field that doesn't have to be clamped to
7934 // the layout scroll range
7935 // - clamp mDestination to the layout scroll range here
7936 // - make sure ComputeScrollMetadata() picks up the former as the
7937 // smooth scroll destination to send to APZ.
7938 mDestination = GetVisualScrollRange().ClampPoint(aVisualViewportOffset);
7940 UniquePtr<ScrollSnapTargetIds> snapTargetIds;
7941 // Perform the scroll.
7942 ApzSmoothScrollTo(mDestination, ScrollMode::SmoothMsd,
7943 aUpdateType == FrameMetrics::eRestore
7944 ? ScrollOrigin::Restore
7945 : ScrollOrigin::Other,
7946 ScrollTriggeredByScript::No, std::move(snapTargetIds));
7947 return true;
7950 bool nsHTMLScrollFrame::IsSmoothScroll(dom::ScrollBehavior aBehavior) const {
7951 if (aBehavior == dom::ScrollBehavior::Instant) {
7952 return false;
7955 // The user smooth scrolling preference should be honored for any requested
7956 // smooth scrolls. A requested smooth scroll when smooth scrolling is
7957 // disabled should be equivalent to an instant scroll. This is not enforced
7958 // for the <scrollbox> XUL element to allow for the browser chrome to
7959 // override this behavior when toolkit.scrollbox.smoothScroll is enabled.
7960 if (!GetContent()->IsXULElement(nsGkAtoms::scrollbox)) {
7961 if (!nsLayoutUtils::IsSmoothScrollingEnabled()) {
7962 return false;
7964 } else {
7965 if (!StaticPrefs::toolkit_scrollbox_smoothScroll()) {
7966 return false;
7970 if (aBehavior == dom::ScrollBehavior::Smooth) {
7971 return true;
7974 nsIFrame* styleFrame = GetFrameForStyle();
7975 if (!styleFrame) {
7976 return false;
7978 return (aBehavior == dom::ScrollBehavior::Auto &&
7979 styleFrame->StyleDisplay()->mScrollBehavior ==
7980 StyleScrollBehavior::Smooth);
7983 nsTArray<ScrollPositionUpdate> nsHTMLScrollFrame::GetScrollUpdates() const {
7984 return mScrollUpdates.Clone();
7987 void nsHTMLScrollFrame::AppendScrollUpdate(
7988 const ScrollPositionUpdate& aUpdate) {
7989 mScrollGeneration = aUpdate.GetGeneration();
7990 mScrollUpdates.AppendElement(aUpdate);
7993 void nsHTMLScrollFrame::ScheduleScrollAnimations() {
7994 nsIContent* content = GetContent();
7995 MOZ_ASSERT(content && content->IsElement(),
7996 "The nsIScrollableFrame should have the element.");
7998 const Element* elementOrPseudo = content->AsElement();
7999 PseudoStyleType pseudo = elementOrPseudo->GetPseudoElementType();
8000 if (pseudo != PseudoStyleType::NotPseudo &&
8001 !AnimationUtils::IsSupportedPseudoForAnimations(pseudo)) {
8002 // This is not an animatable pseudo element, and so we don't generate
8003 // scroll-timeline for it.
8004 return;
8007 const auto [element, type] =
8008 AnimationUtils::GetElementPseudoPair(elementOrPseudo);
8009 const auto* scheduler = ProgressTimelineScheduler::Get(element, type);
8010 if (!scheduler) {
8011 // We don't have scroll timelines associated with this frame.
8012 return;
8015 scheduler->ScheduleAnimations();