Bug 1305957 part 7 - Adjust scroll offset to match change in relative position of scroll anchor after reflow. r=hiro,dbaron
☠☠ backed out by 842b7a62d9ce ☠ ☠
authorRyan Hunt <rhunt@eqrion.net>
Fri, 21 Dec 2018 10:26:10 -0600
changeset 510362 d9052f7b34d9857863b66d64797f8b0b82504bec
parent 510361 e7124fecb7215769a5404c5f2268bbe715162393
child 510363 d9445a5f3458b560fbdb7aee5faabb5a7a9327f3
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewershiro, dbaron
bugs1305957
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1305957 part 7 - Adjust scroll offset to match change in relative position of scroll anchor after reflow. r=hiro,dbaron This commit implements anchor offset adjustment. When the position of a frame that is an anchor is changed during reflow, we notify the anchor container. The anchor container will then post a reflow callback. Then when reflow is completed, the anchor container will perform a scroll to keep the anchor node in the same relative position. Differential Revision: https://phabricator.services.mozilla.com/D13270
layout/base/PresShell.cpp
layout/base/RestyleManager.cpp
layout/generic/ScrollAnchorContainer.cpp
layout/generic/ScrollAnchorContainer.h
layout/generic/nsGfxScrollFrame.cpp
layout/generic/nsGfxScrollFrame.h
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -8612,16 +8612,20 @@ bool PresShell::DoReflow(nsIFrame* targe
   // "window dimensions" code depends on it.
   nsContainerFrame::SyncFrameViewAfterReflow(
       mPresContext, target, target->GetView(), boundsRelativeToTarget);
   nsContainerFrame::SyncWindowProperties(mPresContext, target,
                                          target->GetView(), rcx,
                                          nsContainerFrame::SET_ASYNC);
 
   target->DidReflow(mPresContext, nullptr);
+  if (target->IsInScrollAnchorChain()) {
+    ScrollAnchorContainer* container = ScrollAnchorContainer::FindFor(target);
+    container->ApplyAdjustments();
+  }
   if (isRoot && size.BSize(wm) == NS_UNCONSTRAINEDSIZE) {
     mPresContext->SetVisibleArea(boundsRelativeToTarget);
   }
 
 #ifdef DEBUG
   mCurrentReflowRoot = nullptr;
 #endif
 
--- a/layout/base/RestyleManager.cpp
+++ b/layout/base/RestyleManager.cpp
@@ -768,16 +768,20 @@ static bool RecomputePosition(nsIFrame* 
           cont->AddProperty(nsIFrame::NormalPositionProperty(),
                             new nsPoint(normalPosition));
         }
         cont->SetPosition(normalPosition +
                           nsPoint(newOffsets.left, newOffsets.top));
       }
     }
 
+    if (aFrame->IsInScrollAnchorChain()) {
+      ScrollAnchorContainer* container = ScrollAnchorContainer::FindFor(aFrame);
+      container->ApplyAdjustments();
+    }
     return true;
   }
 
   // For the absolute positioning case, set up a fake HTML reflow state for
   // the frame, and then get the offsets and size from it. If the frame's size
   // doesn't need to change, we can simply update the frame position. Otherwise
   // we fall back to a reflow.
   RefPtr<gfxContext> rc =
@@ -873,16 +877,20 @@ static bool RecomputePosition(nsIFrame* 
 
     // Move the frame
     nsPoint pos(parentBorder.left + reflowInput.ComputedPhysicalOffsets().left +
                     reflowInput.ComputedPhysicalMargin().left,
                 parentBorder.top + reflowInput.ComputedPhysicalOffsets().top +
                     reflowInput.ComputedPhysicalMargin().top);
     aFrame->SetPosition(pos);
 
+    if (aFrame->IsInScrollAnchorChain()) {
+      ScrollAnchorContainer* container = ScrollAnchorContainer::FindFor(aFrame);
+      container->ApplyAdjustments();
+    }
     return true;
   }
 
   // Fall back to a reflow
   StyleChangeReflow(aFrame, nsChangeHint_NeedReflow |
                                 nsChangeHint_ReflowChangesSizeOrPosition);
   return false;
 }
--- a/layout/generic/ScrollAnchorContainer.cpp
+++ b/layout/generic/ScrollAnchorContainer.cpp
@@ -15,17 +15,18 @@
 
 namespace mozilla {
 namespace layout {
 
 ScrollAnchorContainer::ScrollAnchorContainer(ScrollFrameHelper* aScrollFrame)
     : mScrollFrame(aScrollFrame),
       mAnchorNode(nullptr),
       mLastAnchorPos(0, 0),
-      mAnchorNodeIsDirty(true) {}
+      mAnchorNodeIsDirty(true),
+      mApplyingAnchorAdjustment(false) {}
 
 ScrollAnchorContainer::~ScrollAnchorContainer() {}
 
 ScrollAnchorContainer* ScrollAnchorContainer::FindFor(nsIFrame* aFrame) {
   aFrame = aFrame->GetParent();
   if (!aFrame) {
     return nullptr;
   }
@@ -176,17 +177,27 @@ void ScrollAnchorContainer::SelectAnchor
                mLastAnchorPos.y);
   } else {
     mLastAnchorPos = nsPoint();
   }
 
   mAnchorNodeIsDirty = false;
 }
 
-void ScrollAnchorContainer::UserScrolled() { InvalidateAnchor(); }
+void ScrollAnchorContainer::UserScrolled() {
+  if (mApplyingAnchorAdjustment) {
+    return;
+  }
+  InvalidateAnchor();
+}
+
+void ScrollAnchorContainer::SuppressAdjustments() {
+  ANCHOR_LOG("Received a scroll anchor suppression for %p.\n", this);
+  mSuppressAnchorAdjustment = true;
+}
 
 void ScrollAnchorContainer::InvalidateAnchor() {
   if (!StaticPrefs::layout_css_scroll_anchoring_enabled()) {
     return;
   }
 
   ANCHOR_LOG("Invalidating scroll anchor %p for %p.\n", mAnchorNode, this);
 
@@ -203,16 +214,73 @@ void ScrollAnchorContainer::Destroy() {
   if (mAnchorNode) {
     SetAnchorFlags(mScrollFrame->mScrolledFrame, mAnchorNode, false);
   }
   mAnchorNode = nullptr;
   mAnchorNodeIsDirty = false;
   mLastAnchorPos = nsPoint();
 }
 
+void ScrollAnchorContainer::ApplyAdjustments() {
+  if (!mAnchorNode || mAnchorNodeIsDirty) {
+    mSuppressAnchorAdjustment = false;
+    ANCHOR_LOG("Ignoring post-reflow (anchor=%p, dirty=%d, container=%p).\n",
+               mAnchorNode, mAnchorNodeIsDirty, this);
+    return;
+  }
+
+  nsPoint current =
+      FindScrollAnchoringBoundingRect(Frame(), mAnchorNode).TopLeft();
+  nsPoint adjustment = current - mLastAnchorPos;
+  nsIntPoint adjustmentDevicePixels =
+      adjustment.ToNearestPixels(Frame()->PresContext()->AppUnitsPerDevPixel());
+
+  ANCHOR_LOG("Anchor has moved from [%d, %d] to [%d, %d].\n", mLastAnchorPos.x,
+             mLastAnchorPos.y, current.x, current.y);
+
+  WritingMode writingMode = Frame()->GetWritingMode();
+
+  // The specification only allows for anchor adjustments in the block
+  // dimension, so remove the other component.
+  if (writingMode.IsVertical()) {
+    adjustmentDevicePixels.y = 0;
+  } else {
+    adjustmentDevicePixels.x = 0;
+  }
+
+  if (adjustmentDevicePixels == nsIntPoint()) {
+    ANCHOR_LOG("Ignoring zero delta anchor adjustment for %p.\n", this);
+    mSuppressAnchorAdjustment = false;
+    return;
+  }
+
+  if (mSuppressAnchorAdjustment) {
+    ANCHOR_LOG("Applying anchor adjustment suppression for %p.\n", this);
+    mSuppressAnchorAdjustment = false;
+    InvalidateAnchor();
+    return;
+  }
+
+  ANCHOR_LOG("Applying anchor adjustment of (%d %d) for %p and anchor %p.\n",
+             adjustment.x, adjustment.y, this, mAnchorNode);
+
+  MOZ_ASSERT(!mApplyingAnchorAdjustment);
+  // We should use AutoRestore here, but that doesn't work with bitfields
+  mApplyingAnchorAdjustment = true;
+  mScrollFrame->ScrollBy(
+      adjustmentDevicePixels, nsIScrollableFrame::DEVICE_PIXELS,
+      nsIScrollableFrame::INSTANT, nullptr, nsGkAtoms::relative);
+  mApplyingAnchorAdjustment = false;
+
+  // The anchor position may not be in the same relative position after
+  // adjustment. Update ourselves so we have consistent state.
+  mLastAnchorPos =
+      FindScrollAnchoringBoundingRect(Frame(), mAnchorNode).TopLeft();
+}
+
 ScrollAnchorContainer::ExamineResult
 ScrollAnchorContainer::ExamineAnchorCandidate(nsIFrame* aFrame) const {
 #ifdef DEBUG_FRAME_DUMP
   nsCString tag = aFrame->ListTag();
   ANCHOR_LOG("\tVisiting frame=%s (%p).\n", tag.get(), aFrame);
 #else
   ANCHOR_LOG("\t\tVisiting frame=%p.\n", aFrame);
 #endif
--- a/layout/generic/ScrollAnchorContainer.h
+++ b/layout/generic/ScrollAnchorContainer.h
@@ -61,16 +61,22 @@ class ScrollAnchorContainer final {
 
   /**
    * Notify the scroll anchor container that its scroll frame has been
    * scrolled by a user and should invalidate itself.
    */
   void UserScrolled();
 
   /**
+   * Notify the scroll anchor container that a reflow has happened and it
+   * should query its anchor to see if a scroll adjustment needs to occur.
+   */
+  void ApplyAdjustments();
+
+  /**
    * Notify this scroll anchor container that its anchor node should be
    * invalidated and recomputed at the next available opportunity.
    */
   void InvalidateAnchor();
 
   /**
    * Notify this scroll anchor container that it will be destroyed along with
    * its parent frame.
@@ -122,14 +128,16 @@ class ScrollAnchorContainer final {
 
   // The last position of the scroll anchor node relative to the scrollable
   // frame. This is used for calculating the distance to scroll to keep the
   // anchor node in the same relative position
   nsPoint mLastAnchorPos;
 
   // True if we should recalculate our anchor node at the next chance
   bool mAnchorNodeIsDirty : 1;
+  // True if we are applying a scroll anchor adjustment
+  bool mApplyingAnchorAdjustment : 1;
 };
 
 }  // namespace layout
 }  // namespace mozilla
 
 #endif  // mozilla_layout_ScrollAnchorContainer_h_
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -1116,16 +1116,22 @@ void nsHTMLScrollFrame::Reflow(nsPresCon
 
   mHelper.UpdatePrevScrolledRect();
 
   aStatus.Reset();  // This type of frame can't be split.
   NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize);
   mHelper.PostOverflowEvent();
 }
 
+void nsHTMLScrollFrame::DidReflow(nsPresContext* aPresContext,
+                                  const ReflowInput* aReflowInput) {
+  nsContainerFrame::DidReflow(aPresContext, aReflowInput);
+  mHelper.mAnchor.ApplyAdjustments();
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 
 #ifdef DEBUG_FRAME_DUMP
 nsresult nsHTMLScrollFrame::GetFrameName(nsAString& aResult) const {
   return MakeFrameName(NS_LITERAL_STRING("HTMLScroll"), aResult);
 }
 #endif
 
--- a/layout/generic/nsGfxScrollFrame.h
+++ b/layout/generic/nsGfxScrollFrame.h
@@ -760,16 +760,18 @@ class nsHTMLScrollFrame : public nsConta
   virtual nscoord GetMinISize(gfxContext* aRenderingContext) override;
   virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override;
   virtual nsresult GetXULPadding(nsMargin& aPadding) override;
   virtual bool IsXULCollapsed() override;
 
   virtual void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
                       const ReflowInput& aReflowInput,
                       nsReflowStatus& aStatus) override;
+  virtual void DidReflow(nsPresContext* aPresContext,
+                         const ReflowInput* aReflowInput) override;
 
   virtual bool ComputeCustomOverflow(nsOverflowAreas& aOverflowAreas) override {
     return mHelper.ComputeCustomOverflow(aOverflowAreas);
   }
 
   bool GetVerticalAlignBaseline(mozilla::WritingMode aWM,
                                 nscoord* aBaseline) const override {
     *aBaseline = GetLogicalBaseline(aWM);