Bug 1286464 part.8 ContentEventHandler::OnQueryTextRectArray() should handle line break before a node r?smaug draft
authorMasayuki Nakano <masayuki@d-toybox.com>
Fri, 29 Jul 2016 20:02:43 +0900
changeset 394870 6ae1d35b3752dee7f0df2e6d730036b73f548c7f
parent 394869 4797e3cfa760a91f43e4b6ef0e78a87d47edfc6c
child 394871 21a607c803875c5542b60df51c808eb163d34654
push id24655
push usermasayuki@d-toybox.com
push dateMon, 01 Aug 2016 08:00:00 +0000
reviewerssmaug
bugs1286464
milestone50.0a1
Bug 1286464 part.8 ContentEventHandler::OnQueryTextRectArray() should handle line break before a node r?smaug Some elements causes a line break before itself. In such case, OnQueryTextRectArray() should append one or two rects for such line breakers. This patch also implements GetLineBreakerRectBefore(). It computes line breaker rect for the given frame. Even if it's a <br> frame, we cannot use its rect because <br> frame height is computed with line height but we need text's height (i.e., font height) and both height and width are 0 if it's in quirks mode and in non-empty line. Therefore, this patch computes it with frame's baseline, font's max-ascent and max-descent. Although, this patch computes a line breaker rect caused by a open tag of a block level element with block frame's rect. However, it shouldn't be used actually as far as possible. Following patches will kill the path to the hack. MozReview-Commit-ID: 9cym04j9NH9
dom/events/ContentEventHandler.cpp
dom/events/ContentEventHandler.h
--- a/dom/events/ContentEventHandler.cpp
+++ b/dom/events/ContentEventHandler.cpp
@@ -1443,68 +1443,172 @@ ContentEventHandler::GetFirstFrameHaving
       GetNodeHavingFlatText(aRange->GetStartParent(), nodePosition.mOffset);
   }
   nsIFrame* firstFrame = nullptr;
   GetFrameForTextRect(nodePosition.mNode, nodePosition.mOffset,
                       true, &firstFrame);
   return FrameAndNodeOffset(firstFrame, nodePosition.mOffset);
 }
 
+ContentEventHandler::FrameRelativeRect
+ContentEventHandler::GetLineBreakerRectBefore(nsIFrame* aFrame)
+{
+  MOZ_ASSERT(ShouldBreakLineBefore(aFrame->GetContent(), mRootContent));
+
+  nsIFrame* frameForFontMetrics = aFrame;
+
+  // If it's not a <br> frame, this method computes the line breaker's rect
+  // outside the frame.  Therefore, we need to compute with parent frame's
+  // font metrics in such case.
+  if (aFrame->GetType() != nsGkAtoms::brFrame && aFrame->GetParent()) {
+    frameForFontMetrics = aFrame->GetParent();
+  }
+
+  // Note that <br> element's rect is decided with line-height but we need
+  // a rect without leading.  Additionally, <br> frame's width and height
+  // are 0 in quirks mode if it's not an empty line.  So, we cannot use
+  // frame rect information even if it's a <br> frame.
+
+  FrameRelativeRect result(aFrame);
+
+  RefPtr<nsFontMetrics> fontMetrics =
+    nsLayoutUtils::GetInflatedFontMetricsForFrame(frameForFontMetrics);
+  if (NS_WARN_IF(!fontMetrics)) {
+    return FrameRelativeRect();
+  }
+
+  const WritingMode kWritingMode = frameForFontMetrics->GetWritingMode();
+  nscoord baseline = aFrame->GetCaretBaseline();
+  if (kWritingMode.IsVertical()) {
+    if (kWritingMode.IsLineInverted()) {
+      result.mRect.x = baseline - fontMetrics->MaxDescent();
+    } else {
+      result.mRect.x = baseline - fontMetrics->MaxAscent();
+    }
+    result.mRect.width = fontMetrics->MaxHeight();
+  } else {
+    result.mRect.y = baseline - fontMetrics->MaxAscent();
+    result.mRect.height = fontMetrics->MaxHeight();
+  }
+
+  // If aFrame isn't a <br> frame, caret should be at outside of it because
+  // the line break is before its open tag.  However, this is a hack for
+  // unusual scenario.  This hack shouldn't be used as far as possible.
+  if (aFrame->GetType() != nsGkAtoms::brFrame) {
+    if (kWritingMode.IsVertical()) {
+      if (kWritingMode.IsLineInverted()) {
+        // above of top-left corner of aFrame.
+        result.mRect.x = 0;
+      } else {
+        // above of top-right corner of aFrame.
+        result.mRect.x = aFrame->GetRect().XMost() - result.mRect.width;
+      }
+      result.mRect.y = -mPresContext->AppUnitsPerDevPixel();
+    } else {
+      // left of top-left corner of aFrame.
+      result.mRect.x = -mPresContext->AppUnitsPerDevPixel();
+      result.mRect.y = 0;
+    }
+  }
+  return result;
+}
+
 nsresult
 ContentEventHandler::OnQueryTextRectArray(WidgetQueryContentEvent* aEvent)
 {
   nsresult rv = Init(aEvent);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   LineBreakType lineBreakType = GetLineBreakType(aEvent);
+  const uint32_t kBRLength = GetBRLength(lineBreakType);
+
   RefPtr<nsRange> range = new nsRange(mRootContent);
 
   LayoutDeviceIntRect rect;
   uint32_t offset = aEvent->mInput.mOffset;
   const uint32_t kEndOffset = offset + aEvent->mInput.mLength;
   while (offset < kEndOffset) {
     rv = SetRangeFromFlatTextOffset(range, offset, 1, lineBreakType, true,
                                     nullptr);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
+    // TODO: If the range is collapsed, that means offset reaches to the end
+    //       of the contents.  We need to do something here.
+
     // get the starting frame
     FrameAndNodeOffset firstFrame = GetFirstFrameHavingFlatTextInRange(range);
     if (NS_WARN_IF(!firstFrame.IsValid())) {
       return NS_ERROR_FAILURE;
     }
 
+    nsIContent* firstContent = firstFrame.mFrame->GetContent();
+    if (NS_WARN_IF(!firstContent)) {
+      return NS_ERROR_FAILURE;
+    }
+
     // get the starting frame rect
     nsRect frameRect(nsPoint(0, 0), firstFrame->GetRect().Size());
     rv = ConvertToRootRelativeOffset(firstFrame, frameRect);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
     AutoTArray<nsRect, 16> charRects;
-    rv = firstFrame->GetCharacterRectsInRange(firstFrame.mStartOffsetInNode,
-                                              kEndOffset - offset, charRects);
-    if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(charRects.IsEmpty())) {
-      return rv;
+
+    bool isLineBreaker = ShouldBreakLineBefore(firstContent, mRootContent);
+    if (isLineBreaker) {
+      // TODO: If the frame isn't <br> and there was previous text frame or
+      //       <br>, we can set the rect to caret rect at the end.  Currently,
+      //       this sets the rect to caret rect at the start of the node.
+      FrameRelativeRect brRect = GetLineBreakerRectBefore(firstFrame);
+      charRects.AppendElement(brRect.RectRelativeTo(firstFrame));
+    } else {
+      rv = firstFrame->GetCharacterRectsInRange(firstFrame.mStartOffsetInNode,
+                                                kEndOffset - offset, charRects);
+      if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(charRects.IsEmpty())) {
+        // XXX: If the node isn't a text node and does not cause a line break,
+        //      we need to recompute with new range, but how?
+        return rv;
+      }
     }
 
-    for (size_t i = 0; i < charRects.Length(); i++) {
+    for (size_t i = 0; i < charRects.Length() && offset < kEndOffset; i++) {
       nsRect charRect = charRects[i];
       charRect.x += frameRect.x;
       charRect.y += frameRect.y;
 
       rect = LayoutDeviceIntRect::FromUnknownRect(
                charRect.ToOutsidePixels(mPresContext->AppUnitsPerDevPixel()));
       EnsureNonEmptyRect(rect);
 
       aEvent->mReply.mRectArray.AppendElement(rect);
       offset++;
+
+      // If it's not a line breaker or the line breaker length is same as
+      // XP line breaker's, we need to do nothing for current character.
+      if (!isLineBreaker || kBRLength == 1) {
+        continue;
+      }
+
+      MOZ_ASSERT(kBRLength == 2);
+
+      // If it's already reached the end of query range, we don't need to do
+      // anymore.
+      if (offset == kEndOffset) {
+        break;
+      }
+
+      // TODO: If the query range is stated between a line breaker, i.e., \r[\n,
+      //       We shouldn't append a rect here.
+      aEvent->mReply.mRectArray.AppendElement(rect);
+      offset++;
     }
   }
   aEvent->mSucceeded = true;
   return NS_OK;
 }
 
 nsresult
 ContentEventHandler::OnQueryTextRect(WidgetQueryContentEvent* aEvent)
@@ -2245,9 +2349,37 @@ ContentEventHandler::OnSelectionEvent(Wi
 
   mSelection->ScrollIntoViewInternal(
     nsISelectionController::SELECTION_FOCUS_REGION,
     false, nsIPresShell::ScrollAxis(), nsIPresShell::ScrollAxis());
   aEvent->mSucceeded = true;
   return NS_OK;
 }
 
+nsRect
+ContentEventHandler::FrameRelativeRect::RectRelativeTo(
+                                          nsIFrame* aDestFrame) const
+{
+  if (!mBaseFrame || NS_WARN_IF(!aDestFrame)) {
+    return nsRect();
+  }
+
+  if (NS_WARN_IF(aDestFrame->PresContext() != mBaseFrame->PresContext())) {
+    return nsRect();
+  }
+
+  if (aDestFrame == mBaseFrame) {
+    return mRect;
+  }
+
+  nsIFrame* rootFrame = mBaseFrame->PresContext()->PresShell()->GetRootFrame();
+  nsRect baseFrameRectInRootFrame =
+    nsLayoutUtils::TransformFrameRectToAncestor(mBaseFrame, nsRect(),
+                                                rootFrame);
+  nsRect destFrameRectInRootFrame =
+    nsLayoutUtils::TransformFrameRectToAncestor(aDestFrame, nsRect(),
+                                                rootFrame);
+  nsPoint difference =
+    destFrameRectInRootFrame.TopLeft() - baseFrameRectInRootFrame.TopLeft();
+  return mRect - difference;
+}
+
 } // namespace mozilla
--- a/dom/events/ContentEventHandler.h
+++ b/dom/events/ContentEventHandler.h
@@ -332,16 +332,49 @@ protected:
     const nsIFrame* operator->() const { return mFrame; }
     operator nsIFrame*() { return mFrame; }
     operator const nsIFrame*() const { return mFrame; }
     bool IsValid() const { return mFrame && mStartOffsetInNode >= 0; }
   };
   // Get first frame in the given range for computing text rect.
   FrameAndNodeOffset GetFirstFrameHavingFlatTextInRange(nsRange* aRange);
 
+  struct FrameRelativeRect final
+  {
+    // mRect is relative to the mBaseFrame's position.
+    nsRect mRect;
+    nsIFrame* mBaseFrame;
+
+    FrameRelativeRect()
+      : mBaseFrame(nullptr)
+    {
+    }
+
+    explicit FrameRelativeRect(nsIFrame* aBaseFrame)
+      : mBaseFrame(aBaseFrame)
+    {
+    }
+
+    FrameRelativeRect(const nsRect& aRect, nsIFrame* aBaseFrame)
+      : mRect(aRect)
+      , mBaseFrame(aBaseFrame)
+    {
+    }
+
+    bool IsValid() const { return mBaseFrame != nullptr; }
+
+    // Returns an nsRect relative to aBaseFrame instead of mBaseFrame.
+    nsRect RectRelativeTo(nsIFrame* aBaseFrame) const;
+  };
+
+  // Returns a rect for line breaker before the node of aFrame.
+  // Note that this doesn't check if aFrame should cause line break in
+  // non-debug build.
+  FrameRelativeRect GetLineBreakerRectBefore(nsIFrame* aFrame);
+
   // Make aRect non-empty.  If width and/or height is 0, these methods set them
   // to 1.  Note that it doesn't set nsRect's width nor height to one device
   // pixel because using nsRect::ToOutsidePixels() makes actual width or height
   // to 2 pixels because x and y may not be aligned to device pixels.
   void EnsureNonEmptyRect(nsRect& aRect) const;
   void EnsureNonEmptyRect(LayoutDeviceIntRect& aRect) const;
 };