Bug 1286464 part.8 ContentEventHandler::OnQueryTextRectArray() should handle line break before a node r=smaug draft
authorMasayuki Nakano <masayuki@d-toybox.com>
Fri, 05 Aug 2016 13:36:22 +0900
changeset 400164 3117d68f056dc69550a16f52adade82049ba8277
parent 400163 aef43e028fdf19b1e4977ce0d73e3fd700793ba9
child 400165 3afaca4d6e9f09e2054c4a6bd876635cfef6c58c
push id26081
push usermasayuki@d-toybox.com
push dateFri, 12 Aug 2016 17:11:29 +0000
reviewerssmaug
bugs1286464
milestone51.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
@@ -1467,70 +1467,187 @@ ContentEventHandler::GetFirstFrameHaving
     }
   }
   nsIFrame* firstFrame = nullptr;
   GetFrameForTextRect(nodePosition.mNode, nodePosition.mOffset,
                       true, &firstFrame);
   return FrameAndNodeOffset(firstFrame, nodePosition.mOffset);
 }
 
+ContentEventHandler::FrameRelativeRect
+ContentEventHandler::GetLineBreakerRectBefore(nsIFrame* aFrame)
+{
+  // Note that this method should be called only with an element's frame whose
+  // open tag causes a line break.
+  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 only with font height.  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.  For example, case of
+  // |<div><p>some text</p></div>|, caret is before <p> element and in <div>
+  // element, the caret should be left of top-left corner of <p> element like:
+  // 
+  // +-<div>-------------------  <div>'s border box
+  // | I +-<p>-----------------  <p>'s border box
+  // | I |
+  // | I |
+  // |   |
+  //   ^- caret
+  //
+  // 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()));
       // Returning empty rect may cause native IME confused, let's make sure to
       // return non-empty rect.
       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)
@@ -2290,9 +2407,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
@@ -337,16 +337,50 @@ 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 MOZ_STACK_CLASS 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 (If aFrame is
+  // a <br> frame or a block level frame, it causes a line break at its
+  // element's open tag, see also ShouldBreakLineBefore()).  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;
 };