From bdc924d25ee6d0bbe6c780c1d88b60db573562e7 Mon Sep 17 00:00:00 2001 From: Moonchild Date: Fri, 30 Sep 2022 12:24:19 +0000 Subject: WIP: basic scroll-anchoring attempt. This is incomplete and won't build due to prerequisites. --- layout/base/nsIPresShell.h | 6 ++ layout/base/nsPresShell.cpp | 32 ++++++- layout/base/nsPresShell.h | 7 +- layout/generic/nsFrame.cpp | 57 +++++++++++ layout/generic/nsFrameStateBits.h | 6 ++ layout/generic/nsGfxScrollFrame.cpp | 184 ++++++++++++++++++++++++++++++++++++ layout/generic/nsGfxScrollFrame.h | 30 ++++++ layout/generic/nsIFrame.h | 4 + layout/generic/nsIScrollableFrame.h | 3 + 9 files changed, 323 insertions(+), 6 deletions(-) diff --git a/layout/base/nsIPresShell.h b/layout/base/nsIPresShell.h index cf19867ef0..d368402e4f 100644 --- a/layout/base/nsIPresShell.h +++ b/layout/base/nsIPresShell.h @@ -485,6 +485,12 @@ public: */ virtual nsCanvasFrame* GetCanvasFrame() const = 0; + /** + * Returns whether the scollable frame need to have a position adjustment + * based on scroll-anchoring. + */ + virtual void ScrollableFrameNeedsAnchorAdjustment(nsIScrollableFrame* aFrame) = 0; + /** * Tell the pres shell that a frame needs to be marked dirty and needs * Reflow. It's OK if this is an ancestor of the frame needing reflow as diff --git a/layout/base/nsPresShell.cpp b/layout/base/nsPresShell.cpp index 23876cc112..0fc84d736c 100644 --- a/layout/base/nsPresShell.cpp +++ b/layout/base/nsPresShell.cpp @@ -1225,6 +1225,7 @@ PresShell::Destroy() } mFramesToDirty.Clear(); + mScrollAnchorAdjustments.Clear(); if (mViewManager) { // Clear the view manager's weak pointer back to |this| in case it @@ -2049,6 +2050,11 @@ PresShell::NotifyDestroyingFrame(nsIFrame* aFrame) } mFramesToDirty.RemoveEntry(aFrame); + + nsIScrollableFrame* scrollableFrame = do_QueryFrame(aFrame); + if (scrollableFrame) { + mScrollAnchorAdjustments.RemoveEntry(scrollableFrame); + } } else { // We must delete this property in situ so that its destructor removes the // frame from FrameLayerBuilder::DisplayItemData::mFrameList -- otherwise @@ -2583,6 +2589,12 @@ PresShell::VerifyHasDirtyRootAncestor(nsIFrame* aFrame) } #endif +void +PresShell::ScrollableFrameNeedsAnchorAdjustment(nsIScrollableFrame* aFrame) +{ + mScrollAnchorAdjustments.PutEntry(aFrame); +} + void PresShell::FrameNeedsReflow(nsIFrame *aFrame, IntrinsicDirty aIntrinsicDirty, nsFrameState aBitToAdd, @@ -4106,12 +4118,21 @@ PresShell::FlushPendingNotifications(mozilla::ChangesToFlush aFlush) didLayoutFlush = true; mFrameConstructor->RecalcQuotesAndCounters(); viewManager->FlushDelayedResize(true); - if (ProcessReflowCommands(flushType < Flush_Layout) && mContentToScrollTo) { - // We didn't get interrupted. Go ahead and scroll to our content - DoScrollContentIntoView(); + if (ProcessReflowCommands(flushType < Flush_Layout)) { + // Apply scroll offset updates for scroll anchors. + for (auto iter = mScrollAnchorAdjustments.Iter(); !iter.Done(); iter.Next()) { + nsIScrollableFrame* frame = iter.Get()->GetKey(); + frame->ApplyScrollAnchorOffsetAdjustment(); + } + mScrollAnchorAdjustments.Clear(); + if (mContentToScrollTo) { - mContentToScrollTo->DeleteProperty(nsGkAtoms::scrolling); - mContentToScrollTo = nullptr; + // We didn't get interrupted; go ahead and scroll to our content. + DoScrollContentIntoView(); + if (mContentToScrollTo) { + mContentToScrollTo->DeleteProperty(nsGkAtoms::scrolling); + mContentToScrollTo = nullptr; + } } } } @@ -10833,6 +10854,7 @@ PresShell::AddSizeOfIncludingThis(MallocSizeOf aMallocSizeOf, } *aPresShellSize += mApproximatelyVisibleFrames.ShallowSizeOfExcludingThis(aMallocSizeOf); *aPresShellSize += mFramesToDirty.ShallowSizeOfExcludingThis(aMallocSizeOf); + *aPresShellSize += mScrollAnchorAdjustments.ShallowSizeOfExcludingThis(aMallocSizeOf); *aPresShellSize += aArenaObjectsSize->mOther; if (nsStyleSet* styleSet = StyleSet()->GetAsGecko()) { diff --git a/layout/base/nsPresShell.h b/layout/base/nsPresShell.h index fbbcfc7ecd..de638f4bf6 100644 --- a/layout/base/nsPresShell.h +++ b/layout/base/nsPresShell.h @@ -129,6 +129,8 @@ public: virtual nsIPageSequenceFrame* GetPageSequenceFrame() const override; virtual nsCanvasFrame* GetCanvasFrame() const override; + virtual void ScrollableFrameNeedsAnchorAdjustment(nsIScrollableFrame* aFrame) override; + virtual void FrameNeedsReflow(nsIFrame *aFrame, IntrinsicDirty aIntrinsicDirty, nsFrameState aBitToAdd, ReflowRootHandling aRootHandling = @@ -427,9 +429,10 @@ public: void SetNextPaintCompressed() { mNextPaintCompressed = true; } -protected: virtual ~PresShell(); +protected: + void HandlePostedReflowCallbacks(bool aInterruptible); void CancelPostedReflowCallbacks(); @@ -852,6 +855,8 @@ protected: // Set of frames that we should mark with NS_FRAME_HAS_DIRTY_CHILDREN after // we finish reflowing mCurrentReflowRoot. nsTHashtable > mFramesToDirty; + + nsTHashtable > mScrollAnchorAdjustments; // Reflow roots that need to be reflowed. nsTArray mDirtyRoots; diff --git a/layout/generic/nsFrame.cpp b/layout/generic/nsFrame.cpp index 43ad970890..5524b726de 100644 --- a/layout/generic/nsFrame.cpp +++ b/layout/generic/nsFrame.cpp @@ -688,6 +688,20 @@ nsFrame::DestroyFrom(nsIFrame* aDestructRoot) } } + if (HasAnyStateBits(NS_FRAME_IS_SCROLL_ANCHOR)) { + // Find the nearest scroll frame, that's the one that marked us as a + // scroll anchor + nsIFrame* currentFrame = this; + while (currentFrame) { + nsIScrollableFrame* scrollTarget = currentFrame->GetScrollTargetFrame(); + if (scrollTarget) { + scrollTarget->ScrollAnchorWillDestroy(); + break; + } + currentFrame = currentFrame->GetParent(); + } + } + if (HasCSSAnimations() || HasCSSTransitions() || EffectSet::GetEffectSet(this)) { // If no new frame for this element is created by the end of the @@ -814,6 +828,35 @@ AddAndRemoveImageAssociations(nsFrame* aFrame, } } +void +nsIFrame::MaybeNotifyScrollAnchor() +{ + bool isScrollAnchor = HasAnyStateBits(NS_FRAME_IS_SCROLL_ANCHOR); + bool containsScrollAnchor = HasAnyStateBits(NS_FRAME_CONTAINS_SCROLL_ANCHOR); + + if (isScrollAnchor) { + printf_stderr("nsIFrame(%p)::MaybeNotifyScrollAnchor scroll anchor offset changed, queueing adjustment.\n", this); + } + if (containsScrollAnchor) { + printf_stderr("nsIFrame(%p)::MaybeNotifyScrollAnchor offset for scroll anchor container changed, queueing adjustment.\n", this); + } + MOZ_ASSERT(!(isScrollAnchor && containsScrollAnchor)); + + if (isScrollAnchor || containsScrollAnchor) { + // Find the nearest scroll frame, that's the one that marked us as a + // scroll anchor + nsIFrame* currentFrame = this; + while (currentFrame) { + nsIScrollableFrame* scrollTarget = currentFrame->GetScrollTargetFrame(); + if (scrollTarget) { + PresShell().ScrollableFrameNeedsAnchorAdjustment(scrollTarget); + break; + } + currentFrame = currentFrame->GetParent(); + } + } +} + // Subclass hook for style post processing /* virtual */ void nsFrame::DidSetStyleContext(nsStyleContext* aOldStyleContext) @@ -2902,6 +2945,13 @@ nsIFrame::BuildDisplayListForChild(nsDisplayListBuilder* aBuilder, aBuilder->AdjustWindowDraggingRegion(child); child->BuildDisplayList(aBuilder, aLists); +/* // Visualise scroll anchor + if (child->HasAnyStateBits(NS_FRAME_IS_SCROLL_ANCHOR)) { + nsRect bounds = child->GetContentRectRelativeToSelf() + + aBuilder->ToReferenceFrame(child); + list.AppendToTop(MakeDisplayItem(aBuilder, child, bounds, NS_RGBA(255, 0, 0, 55))); + } + // */ aBuilder->DisplayCaret(child, aLists.Content()); #ifdef DEBUG DisplayDebugBorders(aBuilder, child, aLists); @@ -2916,6 +2966,13 @@ nsIFrame::BuildDisplayListForChild(nsDisplayListBuilder* aBuilder, nsDisplayListCollection pseudoStack(aBuilder); aBuilder->AdjustWindowDraggingRegion(child); child->BuildDisplayList(aBuilder, pseudoStack); +/* // Visualise scroll anchor + if (child->HasAnyStateBits(NS_FRAME_IS_SCROLL_ANCHOR)) { + nsRect bounds = child->GetContentRectRelativeToSelf() + + aBuilder->ToReferenceFrame(child); + list.AppendToTop(MakeDisplayItem(aBuilder, child, bounds, NS_RGBA(255, 0, 0, 55))); + } + // */ aBuilder->DisplayCaret(child, pseudoStack.Content()); list.AppendToTop(pseudoStack.BorderBackground()); diff --git a/layout/generic/nsFrameStateBits.h b/layout/generic/nsFrameStateBits.h index ba43e37d45..03abab0b9a 100644 --- a/layout/generic/nsFrameStateBits.h +++ b/layout/generic/nsFrameStateBits.h @@ -266,6 +266,12 @@ FRAME_STATE_BIT(Generic, 53, NS_FRAME_IS_NONDISPLAY) // Frame has a LayerActivityProperty property FRAME_STATE_BIT(Generic, 54, NS_FRAME_HAS_LAYER_ACTIVITY_PROPERTY) +// For scroll-anchoring +FRAME_STATE_BIT(Generic, 55, NS_FRAME_CONTAINS_SCROLL_ANCHOR) +FRAME_STATE_BIT(Generic, 56, NS_FRAME_IS_SCROLL_ANCHOR) + +// Bit 57 is currently unused + // Set for all descendants of MathML sub/supscript elements (other than the // base frame) to indicate that the SSTY font feature should be used. FRAME_STATE_BIT(Generic, 58, NS_FRAME_MATHML_SCRIPT_DESCENDANT) diff --git a/layout/generic/nsGfxScrollFrame.cpp b/layout/generic/nsGfxScrollFrame.cpp index ff8500b92f..d3b85a18e5 100644 --- a/layout/generic/nsGfxScrollFrame.cpp +++ b/layout/generic/nsGfxScrollFrame.cpp @@ -55,6 +55,7 @@ #include "nsSVGIntegrationUtils.h" #include "nsIScrollPositionListener.h" #include "StickyScrollContainer.h" +#include "nsIFrame.h" #include "nsIFrameInlines.h" #include "gfxPlatform.h" #include "gfxPrefs.h" @@ -2022,6 +2023,7 @@ ScrollFrameHelper::ScrollFrameHelper(nsContainerFrame* aOuter, , mScrolledFrame(nullptr) , mScrollCornerBox(nullptr) , mResizerBox(nullptr) + , mAnchorNode(nullptr) , mOuter(aOuter) , mAsyncScroll(nullptr) , mAsyncSmoothMSDScroll(nullptr) @@ -2033,6 +2035,7 @@ ScrollFrameHelper::ScrollFrameHelper(nsContainerFrame* aOuter, , mScrollPosAtLastPaint(0, 0) , mRestorePos(-1, -1) , mLastPos(-1, -1) + , mLastAnchorPos(0, 0) , mScrollPosForLayerPixelAlignment(-1, -1) , mLastUpdateFramesPos(-1, -1) , mHadDisplayPortAtLastFrameUpdate(false) @@ -2250,6 +2253,146 @@ ScrollFrameHelper::GetScrollPositionCSSPixels() return CSSIntPoint::FromAppUnitsRounded(GetScrollPosition()); } +static void +MarkAsScrollAnchorNode(nsIFrame* aScrolledFrame, nsIFrame* aAnchorNode) +{ + aAnchorNode->AddStateBits(NS_FRAME_IS_SCROLL_ANCHOR); + nsIFrame* containsAnchor = aAnchorNode->GetParent(); + while (containsAnchor && containsAnchor != aScrolledFrame) { + containsAnchor->AddStateBits(NS_FRAME_CONTAINS_SCROLL_ANCHOR); + containsAnchor = containsAnchor->GetParent(); + } +} + +static void +UnmarkAsScrollAnchorNode(nsIFrame* aScrolledFrame, nsIFrame* aAnchorNode) +{ + aAnchorNode->RemoveStateBits(NS_FRAME_IS_SCROLL_ANCHOR); + nsIFrame* containsAnchor = aAnchorNode->GetParent(); + while (containsAnchor && containsAnchor != aScrolledFrame) { + containsAnchor->RemoveStateBits(NS_FRAME_CONTAINS_SCROLL_ANCHOR); + containsAnchor = containsAnchor->GetParent(); + } +} + +static nsRect +FindScrollAnchorRect(nsIFrame* aScrollableFrame, nsIFrame* aAnchorNode) +{ + nsRect rect = aAnchorNode->GetContentRectRelativeToSelf(); + rect.MoveBy(aAnchorNode->GetOffsetTo(aScrollableFrame->GetParent())); + return rect; +} + +void +ScrollFrameHelper::UpdateScrollAnchor() +{ + printf_stderr("\nnsGfxScrollFrame(%p)::UpdateScrollAnchor scrollport=(%d, %d)x(%d, %d)\n", + this, + mScrollPort.x, + mScrollPort.y, + mScrollPort.width, + mScrollPort.height); + MOZ_ASSERT(mScrolledFrame); + + nsIFrame* oldAnchorNode = mAnchorNode; + for (nsIFrame* kid : mScrolledFrame->PrincipalChildList()) { + nsIFrame* candidate = SelectScrollAnchorImpl(kid); + if (candidate) { + mAnchorNode = candidate; + break; + } + } + + if (oldAnchorNode != mAnchorNode) { + if (oldAnchorNode) { + UnmarkAsScrollAnchorNode(mScrolledFrame, oldAnchorNode); + // Make sure we stop highlighting the old anchor + oldAnchorNode->InvalidateFrame(); + } + if (mAnchorNode) { + MarkAsScrollAnchorNode(mScrolledFrame, mAnchorNode); + // Make sure we start highlighting the new anchor + mAnchorNode->InvalidateFrame(); + } + } + + if (mAnchorNode) { + mLastAnchorPos = FindScrollAnchorRect(mOuter, mAnchorNode).TopLeft(); + } else { + mLastAnchorPos = nsPoint(); + } +} + +nsIFrame* +ScrollFrameHelper::SelectScrollAnchorImpl(nsIFrame* aNode) +{ + nsRect rect = FindScrollAnchorRect(mOuter, aNode); + + bool excludedSubtree = + aNode->IsTransformed() || + aNode->GetType() == mozilla::LayoutFrameType::Placeholder || + aNode->GetType() == mozilla::LayoutFrameType::Inline || + aNode->GetType() == mozilla::LayoutFrameType::Line || + aNode->IsRelativelyPositioned(); // should exclude only sticky, this was easier + bool opaqueSubtree = aNode->GetType() == mozilla::LayoutFrameType::Scroll; + + nsCString tag; + nsIFrame::ListTag(tag, aNode); + + if (excludedSubtree) { + printf_stderr("Found excluded subtree [frame=%s]\n", + tag.get()); + return nullptr; + } + + bool fullyVisible = mScrollPort.Contains(rect) && !rect.IsEmpty(); + + // If this frame is fully visible then select it as the scroll anchor + if (fullyVisible) { + printf_stderr("Found contained frame [frame=%s rect=(%d, %d)x(%d, %d)]\n", + tag.get(), + rect.x, + rect.y, + rect.width, + rect.height); + + return aNode; + } + + // If this frame is partially visible then examine its children for a better + // scroll anchor + if (mScrollPort.Intersects(rect)) { + printf_stderr("Found intersected frame [frame=%s rect=(%d, %d)x(%d, %d)]\n", + tag.get(), + rect.x, + rect.y, + rect.width, + rect.height); + + // This frame is opaque and can be a scroll anchor, but its contents should + // not be scroll anchors + if (opaqueSubtree) { + return aNode; + } + + // Try to find a deeper frame that is fully visible in the scroll port + for (nsIFrame* kid : aNode->PrincipalChildList()) { + nsIFrame* candidate = SelectScrollAnchorImpl(kid); + if (candidate) { + printf_stderr("Using a closer child frame [frame=%p]\n", candidate); + return candidate; + } + } + + printf_stderr("Fallback to intersected frame [frame=%s]\n", + tag.get()); + + return aNode; + } + + return nullptr; +} + /* * this method wraps calls to ScrollToImpl(), either in one shot or incrementally, * based on the setting of the smoothness scroll pref @@ -2833,6 +2976,7 @@ ScrollFrameHelper::ScrollToImpl(nsPoint aPt, const nsRect& aRange, nsIAtom* aOri mScrollGeneration = ++sScrollGenerationCounter; ScrollVisual(); + UpdateScrollAnchor(); bool schedulePaint = true; if (nsLayoutUtils::AsyncPanZoomEnabled(mOuter) && gfxPrefs::APZPaintSkipping()) { @@ -4342,6 +4486,7 @@ ScrollFrameHelper::ReloadChildFrames() mVScrollbarBox = nullptr; mScrollCornerBox = nullptr; mResizerBox = nullptr; + mAnchorNode = nullptr; for (nsIFrame* frame : mOuter->PrincipalChildList()) { nsIContent* content = frame->GetContent(); @@ -6250,3 +6395,42 @@ ScrollFrameHelper::UsesContainerScrolling() const return false; } +void +ScrollFrameHelper::ScrollAnchorWillDestroy() +{ + if (mAnchorNode) { + UnmarkAsScrollAnchorNode(mScrolledFrame, mAnchorNode); + mAnchorNode = nullptr; + mLastAnchorPos = nsPoint(); + // XXX Should we calculate a new one anchor here, or push a callback + // to do that at a safer time? + } +} + +void +ScrollFrameHelper::ApplyScrollAnchorOffsetAdjustment() +{ + MOZ_ASSERT(mAnchorNode); + + nsPoint currentAnchorPos = FindScrollAnchorRect(mOuter, mAnchorNode).TopLeft(); + nsPoint adjustment = currentAnchorPos - mLastAnchorPos; + nsIntPoint adjustmentDevicePixels = adjustment + .ToNearestPixels(mOuter->PresContext()->AppUnitsPerDevPixel()); + + printf_stderr("nsGfxScrollFrame(%p)::ApplyScrollAnchorOffsetAdjustment adjustment=(%d, %d)\n", + this, + adjustmentDevicePixels.x, + adjustmentDevicePixels.y); + + nsIFrame* scrollAnchor = mAnchorNode; + + ScrollBy(adjustmentDevicePixels, + nsIScrollableFrame::DEVICE_PIXELS, + nsIScrollableFrame::INSTANT, + nullptr, + nsGkAtoms::relative); + + // The last scroll anchor offset should be updated within ScrollToImpl and + // the scroll anchor should not have changed + MOZ_ASSERT(scrollAnchor == mAnchorNode); +} \ No newline at end of file diff --git a/layout/generic/nsGfxScrollFrame.h b/layout/generic/nsGfxScrollFrame.h index 41957d5e55..49e22b9224 100644 --- a/layout/generic/nsGfxScrollFrame.h +++ b/layout/generic/nsGfxScrollFrame.h @@ -458,6 +458,11 @@ public: return mSuppressScrollbarRepaints; } + + void ScrollAnchorWillDestroy(); + + void ApplyScrollAnchorOffsetAdjustment(); + // owning references to the nsIAnonymousContentCreator-built content nsCOMPtr mHScrollbarContent; nsCOMPtr mVScrollbarContent; @@ -472,6 +477,7 @@ public: nsIFrame* mScrolledFrame; nsIFrame* mScrollCornerBox; nsIFrame* mResizerBox; + nsIFrame* mAnchorNode; nsContainerFrame* mOuter; RefPtr mAsyncScroll; RefPtr mAsyncSmoothMSDScroll; @@ -500,6 +506,11 @@ public: // other than trying to restore mRestorePos. nsPoint mLastPos; + // The last scroll position of the scroll anchor node. This is in the same + // coordinate space as mScrollPort, that is, relative to the parent of this + // scrollable frame. + nsPoint mLastAnchorPos; + nsExpirationState mActivityExpirationState; nsCOMPtr mScrollActivityTimer; @@ -619,6 +630,9 @@ protected: bool mOldSuppressValue; }; + void UpdateScrollAnchor(); + nsIFrame* SelectScrollAnchorImpl(nsIFrame* aNode); + /** * @note This method might destroy the frame, pres shell and other objects. */ @@ -1047,6 +1061,14 @@ public: virtual mozilla::a11y::AccType AccessibleType() override; #endif + virtual void ScrollAnchorWillDestroy() override { + mHelper.ScrollAnchorWillDestroy(); + } + + virtual void ApplyScrollAnchorOffsetAdjustment() override { + mHelper.ApplyScrollAnchorOffsetAdjustment(); + } + protected: nsHTMLScrollFrame(nsStyleContext* aContext, bool aIsRoot); void SetSuppressScrollbarUpdate(bool aSuppress) { @@ -1471,6 +1493,14 @@ public: virtual nsresult GetFrameName(nsAString& aResult) const override; #endif + virtual void ScrollAnchorWillDestroy() override { + mHelper.ScrollAnchorWillDestroy(); + } + + virtual void ApplyScrollAnchorOffsetAdjustment() override { + mHelper.ApplyScrollAnchorOffsetAdjustment(); + } + protected: nsXULScrollFrame(nsStyleContext* aContext, bool aIsRoot, bool aClipAllDescendants); diff --git a/layout/generic/nsIFrame.h b/layout/generic/nsIFrame.h index 1c5dbbe9ac..db95478bd0 100644 --- a/layout/generic/nsIFrame.h +++ b/layout/generic/nsIFrame.h @@ -861,6 +861,7 @@ public: } else { mRect = aRect; } + MaybeNotifyScrollAnchor(); } /** * Set this frame's rect from a logical rect in its own writing direction @@ -922,6 +923,7 @@ public: // the top right of the frame instead of the top left. mRect.MoveTo(aPt.GetPhysicalPoint(aWritingMode, aContainerSize - mRect.Size())); + MaybeNotifyScrollAnchor(); } /** @@ -2742,6 +2744,8 @@ public: const nsRect* aFrameDamageRect = nullptr, uint32_t aFlags = 0); + void MaybeNotifyScrollAnchor(); + /** * Returns a rect that encompasses everything that might be painted by * this frame. This includes this frame, all its descendant frames, this diff --git a/layout/generic/nsIScrollableFrame.h b/layout/generic/nsIScrollableFrame.h index 4ad45a528d..96b19a1c78 100644 --- a/layout/generic/nsIScrollableFrame.h +++ b/layout/generic/nsIScrollableFrame.h @@ -475,6 +475,9 @@ public: virtual ScrollSnapInfo GetScrollSnapInfo() const = 0; virtual void SetScrollsClipOnUnscrolledOutOfFlow() = 0; + + virtual void ScrollAnchorWillDestroy() = 0; + virtual void ApplyScrollAnchorOffsetAdjustment() = 0; }; #endif -- cgit v1.2.3