From aff17a41ed57f81202c2ce439ff7428b52281d4b Mon Sep 17 00:00:00 2001
From: Jonathan Kew <jkew@mozilla.com>
Date: Thu, 26 May 2022 16:32:10 +0000
Subject: [PATCH] Bug 1770981 - Split the nsBlockFrame "line cursor" into
 separate cursors used for display-list building vs frame queries.
 r=layout-reviewers,Jamie,emilio

Differential Revision: https://phabricator.services.mozilla.com/D147203
---
 layout/generic/nsBlockFrame.cpp |  71 ++++++++++++----------
 layout/generic/nsBlockFrame.h   | 101 ++++++++++++++++++--------------
 layout/generic/nsTextFrame.cpp  |   4 +-
 3 files changed, 100 insertions(+), 76 deletions(-)

diff --git a/layout/generic/nsBlockFrame.cpp b/layout/generic/nsBlockFrame.cpp
index 88b7a70d18601..5b1dbca5d7ad5 100644
--- a/layout/generic/nsBlockFrame.cpp
+++ b/layout/generic/nsBlockFrame.cpp
@@ -472,7 +472,7 @@ void nsBlockFrame::AddSizeOfExcludingThisForTree(
 
 void nsBlockFrame::DestroyFrom(nsIFrame* aDestructRoot,
                                PostDestroyData& aPostDestroyData) {
-  ClearLineCursor();
+  ClearLineCursors();
   DestroyAbsoluteFrames(aDestructRoot, aPostDestroyData);
   mFloats.DestroyFramesFrom(aDestructRoot, aPostDestroyData);
   nsPresContext* presContext = PresContext();
@@ -1338,7 +1338,7 @@ void nsBlockFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aMetrics,
   // because we may invalidate the nondecreasing
   // overflowArea.InkOverflow().y/yMost invariant, and we may even
   // delete the line with the line cursor.
-  ClearLineCursor();
+  ClearLineCursors();
 
   if (IsFrameTreeTooDeep(*reflowInput, aMetrics, aStatus)) {
     return;
@@ -1655,9 +1655,8 @@ void nsBlockFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aMetrics,
       }
       // Setup the line cursor here to optimize line searching for
       // calculating hypothetical position of absolutely-positioned
-      // frames. The line cursor is immediately cleared afterward to
-      // avoid affecting the display list generation.
-      AutoLineCursorSetup autoLineCursor(this);
+      // frames.
+      SetupLineCursorForQuery();
       absoluteContainer->Reflow(this, aPresContext, *reflowInput,
                                 state.mReflowStatus, containingBlock, flags,
                                 &aMetrics.mOverflowAreas);
@@ -2231,7 +2230,7 @@ bool nsBlockFrame::ComputeCustomOverflow(OverflowAreas& aOverflowAreas) {
 
   // Line cursor invariants depend on the overflow areas of the lines, so
   // we must clear the line cursor since those areas may have changed.
-  ClearLineCursor();
+  ClearLineCursors();
   return nsContainerFrame::ComputeCustomOverflow(aOverflowAreas);
 }
 
@@ -3031,8 +3030,13 @@ void nsBlockFrame::ReflowDirtyLines(BlockReflowState& aState) {
         continue;
       }
 
-      if (pulledLine == nextInFlow->GetLineCursor()) {
-        nextInFlow->ClearLineCursor();
+      if (nextInFlow->MaybeHasLineCursor()) {
+        if (pulledLine == nextInFlow->GetLineCursorForDisplay()) {
+          nextInFlow->ClearLineCursorForDisplay();
+        }
+        if (pulledLine == nextInFlow->GetLineCursorForQuery()) {
+          nextInFlow->ClearLineCursorForQuery();
+        }
       }
       ReparentFrames(pulledFrames, nextInFlow, this);
       pulledLine->SetMovedFragments();
@@ -5212,7 +5216,7 @@ bool nsBlockFrame::DrainOverflowLines() {
   bool didFindOverflow = false;
   nsBlockFrame* prevBlock = static_cast<nsBlockFrame*>(GetPrevInFlow());
   if (prevBlock) {
-    prevBlock->ClearLineCursor();
+    prevBlock->ClearLineCursors();
     FrameLines* overflowLines = prevBlock->RemoveOverflowLines();
     if (overflowLines) {
       // Make all the frames on the overflow line list mine.
@@ -5713,7 +5717,7 @@ static bool ShouldPutNextSiblingOnNewLine(nsIFrame* aLastFrame) {
 void nsBlockFrame::AddFrames(nsFrameList& aFrameList, nsIFrame* aPrevSibling,
                              const nsLineList::iterator* aPrevSiblingLine) {
   // Clear our line cursor, since our lines may change.
-  ClearLineCursor();
+  ClearLineCursors();
 
   if (aFrameList.IsEmpty()) {
     return;
@@ -6093,7 +6097,7 @@ nsBlockInFlowLineIterator::nsBlockInFlowLineIterator(nsBlockFrame* aFrame,
   }
 
   // Try to use the cursor if it exists, otherwise fall back to the first line
-  if (nsLineBox* const cursor = aFrame->GetLineCursor()) {
+  if (nsLineBox* const cursor = aFrame->GetLineCursorForQuery()) {
     mLine = line_end;
     // Perform a simultaneous forward and reverse search starting from the
     // line cursor.
@@ -6123,7 +6127,7 @@ nsBlockInFlowLineIterator::nsBlockInFlowLineIterator(nsBlockFrame* aFrame,
     if (mLine != line_end) {
       *aFoundValidLine = true;
       if (mLine != cursor) {
-        aFrame->SetProperty(nsBlockFrame::LineCursorProperty(), mLine);
+        aFrame->SetProperty(nsBlockFrame::LineCursorPropertyQuery(), mLine);
       }
       return;
     }
@@ -6244,7 +6248,7 @@ void nsBlockFrame::DoRemoveFrameInternal(nsIFrame* aDeletedFrame,
                                          uint32_t aFlags,
                                          PostDestroyData& aPostDestroyData) {
   // Clear our line cursor, since our lines may change.
-  ClearLineCursor();
+  ClearLineCursors();
 
   if (aDeletedFrame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW |
                                      NS_FRAME_IS_OVERFLOW_CONTAINER)) {
@@ -7168,7 +7172,7 @@ void nsBlockFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
     }
 
     if (nonDecreasingYs && lineCount >= MIN_LINES_NEEDING_CURSOR) {
-      SetupLineCursor();
+      SetupLineCursorForDisplay();
     }
 
     if (!curBackplateArea.IsEmpty()) {
@@ -7243,30 +7247,31 @@ a11y::AccType nsBlockFrame::AccessibleType() {
 }
 #endif
 
-void nsBlockFrame::ClearLineCursor() {
-  if (!HasAnyStateBits(NS_BLOCK_HAS_LINE_CURSOR)) {
+void nsBlockFrame::SetupLineCursorForDisplay() {
+  if (mLines.empty() || HasProperty(LineCursorPropertyDisplay())) {
     return;
   }
 
-  RemoveProperty(LineCursorProperty());
-  RemoveStateBits(NS_BLOCK_HAS_LINE_CURSOR);
+  SetProperty(LineCursorPropertyDisplay(), mLines.front());
+  AddStateBits(NS_BLOCK_HAS_LINE_CURSOR);
 }
 
-void nsBlockFrame::SetupLineCursor() {
-  if (HasAnyStateBits(NS_BLOCK_HAS_LINE_CURSOR) || mLines.empty()) {
+void nsBlockFrame::SetupLineCursorForQuery() {
+  if (mLines.empty() || HasProperty(LineCursorPropertyQuery())) {
     return;
   }
 
-  SetProperty(LineCursorProperty(), mLines.front());
+  SetProperty(LineCursorPropertyQuery(), mLines.front());
   AddStateBits(NS_BLOCK_HAS_LINE_CURSOR);
 }
 
 nsLineBox* nsBlockFrame::GetFirstLineContaining(nscoord y) {
-  if (!HasAnyStateBits(NS_BLOCK_HAS_LINE_CURSOR)) {
+  // Although this looks like a "querying" method, it is used by the
+  // display-list building code, so uses the Display cursor.
+  nsLineBox* property = GetLineCursorForDisplay();
+  if (!property) {
     return nullptr;
   }
-
-  nsLineBox* property = GetProperty(LineCursorProperty());
   LineIterator cursor = mLines.begin(property);
   nsRect cursorArea = cursor->InkOverflowRect();
 
@@ -7282,7 +7287,7 @@ nsLineBox* nsBlockFrame::GetFirstLineContaining(nscoord y) {
   }
 
   if (cursor.get() != property) {
-    SetProperty(LineCursorProperty(), cursor.get());
+    SetProperty(LineCursorPropertyDisplay(), cursor.get());
   }
 
   return cursor.get();
@@ -8019,7 +8024,7 @@ void nsBlockFrame::VerifyLines(bool aFinalCheckOK) {
     return;
   }
 
-  nsLineBox* cursor = GetLineCursor();
+  nsLineBox* cursor = GetLineCursorForQuery();
 
   // Add up the counts on each line. Also validate that IsFirstLine is
   // set properly.
@@ -8134,8 +8139,10 @@ void nsBlockFrame::VerifyOverflowSituation() {
                        overflowLines->mFrames.FirstChild(),
                    "bad overflow frames / lines");
     }
-    nsLineBox* cursor = flow->GetLineCursor();
-    if (cursor) {
+    auto checkCursor = [&](nsLineBox* cursor) -> bool {
+      if (!cursor) {
+        return true;
+      }
       LineIterator line = flow->LinesBegin();
       LineIterator line_end = flow->LinesEnd();
       for (; line != line_end && line != cursor; ++line)
@@ -8146,8 +8153,12 @@ void nsBlockFrame::VerifyOverflowSituation() {
         for (; line != line_end && line != cursor; ++line)
           ;
       }
-      MOZ_ASSERT(line != line_end, "stale LineCursorProperty");
-    }
+      return line != line_end;
+    };
+    MOZ_ASSERT(checkCursor(flow->GetLineCursorForDisplay()),
+               "stale LineCursorPropertyDisplay");
+    MOZ_ASSERT(checkCursor(flow->GetLineCursorForQuery()),
+               "stale LineCursorPropertyQuery");
     flow = static_cast<nsBlockFrame*>(flow->GetNextInFlow());
   }
 }
diff --git a/layout/generic/nsBlockFrame.h b/layout/generic/nsBlockFrame.h
index 7197cf11d2949..b896a943c8c07 100644
--- a/layout/generic/nsBlockFrame.h
+++ b/layout/generic/nsBlockFrame.h
@@ -175,12 +175,10 @@ class nsBlockFrame : public nsContainerFrame {
   mozilla::a11y::AccType AccessibleType() override;
 #endif
 
-  // Line cursor methods to speed up line searching in which one query
-  // result is expected to be close to the next in general. This is
-  // mainly for searching line(s) containing a point. It is also used
-  // as a cache for local computation. Use AutoLineCursorSetup for the
-  // latter case so that it wouldn't interact unexpectedly with the
-  // former. The basic idea for the former is that we set the cursor
+  // Line cursor methods to speed up line searching in which one query result
+  // is expected to be close to the next in general. This is mainly for
+  // searching line(s) containing a point. It is also used as a cache for local
+  // computation. The basic idea for the former is that we set the cursor
   // property if the lines' overflowArea.InkOverflow().ys and
   // overflowArea.InkOverflow().yMosts are non-decreasing
   // (considering only non-empty overflowArea.InkOverflow()s; empty
@@ -191,45 +189,46 @@ class nsBlockFrame : public nsContainerFrame {
   // "near" the cursor, then we can find those nearby lines quickly by
   // starting our search at the cursor.
 
-  // Clear out line cursor because we're disturbing the lines (i.e., Reflow)
-  void ClearLineCursor();
+  // We have two independent line cursors, one used for display-list building
+  // and the other for a11y or other frame queries. Either or both may be
+  // present at any given time. When we reflow or otherwise munge the lines,
+  // both cursors will be cleared.
+  // The display cursor is only created and used if the lines satisfy the non-
+  // decreasing y-coordinate condition (see SetupLineCursorForDisplay comment
+  // below), whereas the query cursor may be created for any block. The two
+  // are separated so creating a cursor for a11y queries (eg GetRenderedText)
+  // does not risk confusing the display-list building code.
+
+  // Clear out line cursors because we're disturbing the lines (i.e., Reflow)
+  void ClearLineCursors() {
+    if (MaybeHasLineCursor()) {
+      ClearLineCursorForDisplay();
+      ClearLineCursorForQuery();
+      RemoveStateBits(NS_BLOCK_HAS_LINE_CURSOR);
+    }
+  }
+  void ClearLineCursorForDisplay() {
+    RemoveProperty(LineCursorPropertyDisplay());
+  }
+  void ClearLineCursorForQuery() { RemoveProperty(LineCursorPropertyQuery()); }
+
   // Get the first line that might contain y-coord 'y', or nullptr if you must
   // search all lines. If nonnull is returned then we guarantee that the lines'
   // combinedArea.ys and combinedArea.yMosts are non-decreasing.
   // The actual line returned might not contain 'y', but if not, it is
   // guaranteed to be before any line which does contain 'y'.
   nsLineBox* GetFirstLineContaining(nscoord y);
-  // Set the line cursor to our first line. Only call this if you
-  // guarantee that either the lines' combinedArea.ys and combinedArea.
-  // yMosts are non-decreasing, or the line cursor is cleared before
-  // building the display list of this frame.
-  void SetupLineCursor();
 
-  /**
-   * Helper RAII class for automatically set and clear line cursor for
-   * temporary use. If the frame already has line cursor, this would be
-   * a no-op.
-   */
-  class MOZ_STACK_CLASS AutoLineCursorSetup {
-   public:
-    explicit AutoLineCursorSetup(nsBlockFrame* aFrame)
-        : mFrame(aFrame), mOrigCursor(aFrame->GetLineCursor()) {
-      if (!mOrigCursor) {
-        mFrame->SetupLineCursor();
-      }
-    }
-    ~AutoLineCursorSetup() {
-      if (mOrigCursor) {
-        mFrame->SetProperty(LineCursorProperty(), mOrigCursor);
-      } else {
-        mFrame->ClearLineCursor();
-      }
-    }
+  // Ensure the frame has a display-list line cursor, initializing it to the
+  // first line if it is not already present. (If there's an existing cursor,
+  // it is left untouched.) Only call this if you guarantee that the lines'
+  // combinedArea.ys and combinedArea.yMosts are non-decreasing.
+  void SetupLineCursorForDisplay();
 
-   private:
-    nsBlockFrame* mFrame;
-    nsLineBox* mOrigCursor;
-  };
+  // Ensure the frame has a query line cursor, initializing it to the first
+  // line if it is not already present. (If there's an existing cursor, it is
+  // left untouched.)
+  void SetupLineCursorForQuery();
 
   void ChildIsDirty(nsIFrame* aChild) override;
 
@@ -424,10 +423,23 @@ class nsBlockFrame : public nsContainerFrame {
       nsPresContext* aPresContext);
 #endif
 
-  NS_DECLARE_FRAME_PROPERTY_WITHOUT_DTOR(LineCursorProperty, nsLineBox)
-  bool HasLineCursor() { return HasAnyStateBits(NS_BLOCK_HAS_LINE_CURSOR); }
-  nsLineBox* GetLineCursor() {
-    return HasLineCursor() ? GetProperty(LineCursorProperty()) : nullptr;
+  NS_DECLARE_FRAME_PROPERTY_WITHOUT_DTOR(LineCursorPropertyDisplay, nsLineBox)
+  NS_DECLARE_FRAME_PROPERTY_WITHOUT_DTOR(LineCursorPropertyQuery, nsLineBox)
+  // Note that the NS_BLOCK_HAS_LINE_CURSOR flag does not necessarily mean the
+  // cursor is present, as it covers both the "display" and "query" cursors,
+  // but may remain set if they have been separately deleted. In such a case,
+  // the Get* accessors will be slightly more expensive, but will still safely
+  // return null if the cursor is absent.
+  bool MaybeHasLineCursor() {
+    return HasAnyStateBits(NS_BLOCK_HAS_LINE_CURSOR);
+  }
+  nsLineBox* GetLineCursorForDisplay() {
+    return MaybeHasLineCursor() ? GetProperty(LineCursorPropertyDisplay())
+                                : nullptr;
+  }
+  nsLineBox* GetLineCursorForQuery() {
+    return MaybeHasLineCursor() ? GetProperty(LineCursorPropertyQuery())
+                                : nullptr;
   }
 
   nsLineBox* NewLineBox(nsIFrame* aFrame, bool aIsBlock) {
@@ -438,8 +450,11 @@ class nsBlockFrame : public nsContainerFrame {
     return NS_NewLineBox(PresShell(), aFromLine, aFrame, aCount);
   }
   void FreeLineBox(nsLineBox* aLine) {
-    if (aLine == GetLineCursor()) {
-      ClearLineCursor();
+    if (aLine == GetLineCursorForDisplay()) {
+      ClearLineCursorForDisplay();
+    }
+    if (aLine == GetLineCursorForQuery()) {
+      ClearLineCursorForQuery();
     }
     aLine->Destroy(PresShell());
   }
diff --git a/layout/generic/nsTextFrame.cpp b/layout/generic/nsTextFrame.cpp
index 53cae3b2c16f8..6763ef401d9f7 100644
--- a/layout/generic/nsTextFrame.cpp
+++ b/layout/generic/nsTextFrame.cpp
@@ -10199,7 +10199,6 @@ nsIFrame::RenderedText nsTextFrame::GetRenderedText(
   uint32_t offsetInRenderedString = 0;
   bool haveOffsets = false;
 
-  Maybe<nsBlockFrame::AutoLineCursorSetup> autoLineCursor;
   for (textFrame = this; textFrame;
        textFrame = textFrame->GetNextContinuation()) {
     if (textFrame->HasAnyStateBits(NS_FRAME_IS_DIRTY)) {
@@ -10225,8 +10224,7 @@ nsIFrame::RenderedText nsTextFrame::GetRenderedText(
       if (thisLc != lineContainer) {
         // Setup line cursor when needed.
         lineContainer = thisLc;
-        autoLineCursor.reset();
-        autoLineCursor.emplace(lineContainer);
+        lineContainer->SetupLineCursorForQuery();
       }
       LineStartsOrEndsAtHardLineBreak(textFrame, lineContainer,
                                       &startsAtHardBreak, &endsAtHardBreak);
-- 
GitLab