Bug 1286464 part.19 ContentEventHandler::OnQueryTextRect() should handle the case when queried range starts from the end of mRootContent r=smaug
First, when the first node causing text is invisible, OnQueryTextRect() still fails even with this patch. We shouldn't fix it in this bug because it's unusual case but this bug is very important especially for some web service using HTML editor like Gmail.
This patch fixes all cases when the start offset of queried range reaches the end of mRootContent.
1. When the last node causing text is a <br> element (either content <br> element or moz-<br> element), its frame is a placeholder for empty line. Therefore, this patch sets the rect to the frame rect.
2. When the last node causing text is a text node, the last frame generated for it represents its line (including empty line). Therefore, this patch sets the rect to the result of GetLineBreakerRectAfter().
3. When the last node causes a line breaker before it, the frame may be a placeholder for it (this is not usual case, when user types Enter key at the end of <p> element, <p><br></p> is generated by Gecko). In this case, this patch sets a possible caret rect which is guessed from the content box of the frame and its font height.
4. When there are no nodes causing text in mRootContent, this patch sets a possible caret rect like case #3.
MozReview-Commit-ID: FS9cWJQ39DK
--- a/dom/events/ContentEventHandler.cpp
+++ b/dom/events/ContentEventHandler.cpp
@@ -659,21 +659,37 @@ ContentEventHandler::ShouldBreakLineBefo
// If the element is unknown element, we shouldn't insert line breaks before
// it since unknown elements should be ignored.
RefPtr<HTMLUnknownElement> unknownHTMLElement = do_QueryObject(aContent);
return !unknownHTMLElement;
}
nsresult
+ContentEventHandler::GenerateFlatTextContent(nsIContent* aContent,
+ nsAFlatString& aString,
+ LineBreakType aLineBreakType)
+{
+ MOZ_ASSERT(aString.IsEmpty());
+
+ RefPtr<nsRange> range = new nsRange(mRootContent);
+ ErrorResult rv;
+ range->SelectNodeContents(*aContent, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+ return GenerateFlatTextContent(range, aString, aLineBreakType);
+}
+
+nsresult
ContentEventHandler::GenerateFlatTextContent(nsRange* aRange,
nsAFlatString& aString,
LineBreakType aLineBreakType)
{
- NS_ASSERTION(aString.IsEmpty(), "aString must be empty string");
+ MOZ_ASSERT(aString.IsEmpty());
if (aRange->Collapsed()) {
return NS_OK;
}
nsINode* startNode = aRange->GetStartParent();
nsINode* endNode = aRange->GetEndParent();
if (NS_WARN_IF(!startNode) || NS_WARN_IF(!endNode)) {
@@ -1717,16 +1733,59 @@ ContentEventHandler::GuessLineBreakerRec
// Right of the last text frame (not bidi-aware).
result.mRect.SetRect(kLastTextFrameRect.width, 0,
0, kLastTextFrameRect.height);
}
result.mBaseFrame = lastTextFrame;
return result;
}
+ContentEventHandler::FrameRelativeRect
+ContentEventHandler::GuessFirstCaretRectIn(nsIFrame* aFrame)
+{
+ const WritingMode kWritingMode = aFrame->GetWritingMode();
+
+ // Computes the font height, but if it's not available, we should use
+ // default font size of Firefox. The default font size in default settings
+ // is 16px.
+ RefPtr<nsFontMetrics> fontMetrics =
+ nsLayoutUtils::GetInflatedFontMetricsForFrame(aFrame);
+ const nscoord kMaxHeight =
+ fontMetrics ? fontMetrics->MaxHeight() :
+ 16 * mPresContext->AppUnitsPerDevPixel();
+
+ nsRect caretRect;
+ const nsRect kContentRect = aFrame->GetContentRect() - aFrame->GetPosition();
+ caretRect.y = kContentRect.y;
+ if (!kWritingMode.IsVertical()) {
+ if (kWritingMode.IsBidiLTR()) {
+ caretRect.x = kContentRect.x;
+ } else {
+ // Move 1px left for the space of caret itself.
+ const nscoord kOnePixel = mPresContext->AppUnitsPerDevPixel();
+ caretRect.x = kContentRect.XMost() - kOnePixel;
+ }
+ caretRect.height = kMaxHeight;
+ // However, don't add kOnePixel here because it may cause 2px width at
+ // aligning the edge to device pixels.
+ caretRect.width = 1;
+ } else {
+ if (kWritingMode.IsVerticalLR()) {
+ caretRect.x = kContentRect.x;
+ } else {
+ caretRect.x = kContentRect.XMost() - kMaxHeight;
+ }
+ caretRect.width = kMaxHeight;
+ // Don't add app units for a device pixel because it may cause 2px height
+ // at aligning the edge to device pixels.
+ caretRect.height = 1;
+ }
+ return FrameRelativeRect(caretRect, aFrame);
+}
+
nsresult
ContentEventHandler::OnQueryTextRectArray(WidgetQueryContentEvent* aEvent)
{
nsresult rv = Init(aEvent);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
@@ -1753,20 +1812,33 @@ ContentEventHandler::OnQueryTextRectArra
// contents.
if (range->Collapsed()) {
break;
}
// Get the first frame which causes some text after the offset.
FrameAndNodeOffset firstFrame = GetFirstFrameInRangeForTextRect(range);
- // If GetFirstFrameInRangeForTextRect() does not return valid frame,
- // that means that there is no remaining content which causes text.
- // So, in such case, we must have reached the end of the contents.
+ // If GetFirstFrameInRangeForTextRect() does not return valid frame, that
+ // means that there are no visible frames having text or the offset reached
+ // the end of contents.
if (!firstFrame.IsValid()) {
+ nsAutoString allText;
+ rv = GenerateFlatTextContent(mRootContent, allText, lineBreakType);
+ // If the offset doesn't reach the end of contents yet but there is no
+ // frames for the node, that means that current offset's node is hidden
+ // by CSS or something. Ideally, we should handle it with the last
+ // visible text node's last character's rect, but it's not usual cases
+ // in actual web services. Therefore, currently, we should make this
+ // case fail.
+ if (NS_WARN_IF(NS_FAILED(rv)) || offset < allText.Length()) {
+ return NS_ERROR_FAILURE;
+ }
+ // Otherwise, we should append caret rect at the end of the contents
+ // later.
break;
}
nsIContent* firstContent = firstFrame.mFrame->GetContent();
if (NS_WARN_IF(!firstContent)) {
return NS_ERROR_FAILURE;
}
@@ -2040,22 +2112,95 @@ ContentEventHandler::OnQueryTextRect(Wid
// used to iterate over all contents and their frames
nsCOMPtr<nsIContentIterator> iter = NS_NewContentIterator();
iter->Init(range);
// Get the first frame which causes some text after the offset.
FrameAndNodeOffset firstFrame = GetFirstFrameInRangeForTextRect(range);
- // If GetFirstFrameInRangeForTextRect() does not return valid frame,
- // that means that there is no remaining content which causes text.
- // So, in such case, we must have reached the end of the contents.
+ // If GetFirstFrameInRangeForTextRect() does not return valid frame, that
+ // means that there are no visible frames having text or the offset reached
+ // the end of contents.
if (!firstFrame.IsValid()) {
- // TODO: Handle this case later.
- return NS_ERROR_FAILURE;
+ nsAutoString allText;
+ rv = GenerateFlatTextContent(mRootContent, allText, lineBreakType);
+ // If the offset doesn't reach the end of contents but there is no frames
+ // for the node, that means that current offset's node is hidden by CSS or
+ // something. Ideally, we should handle it with the last visible text
+ // node's last character's rect, but it's not usual cases in actual web
+ // services. Therefore, currently, we should make this case fail.
+ if (NS_WARN_IF(NS_FAILED(rv)) ||
+ static_cast<uint32_t>(aEvent->mInput.mOffset) < allText.Length()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Look for the last frame which should be included text rects.
+ ErrorResult erv;
+ range->SelectNodeContents(*mRootContent, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ nsRect rect;
+ FrameAndNodeOffset lastFrame = GetLastFrameInRangeForTextRect(range);
+ // If there is at least one frame which can be used for computing a rect
+ // for a character or a line breaker, we should use it for guessing the
+ // caret rect at the end of the contents.
+ if (lastFrame) {
+ if (NS_WARN_IF(!lastFrame->GetContent())) {
+ return NS_ERROR_FAILURE;
+ }
+ FrameRelativeRect relativeRect;
+ // If there is a <br> frame at the end, it represents an empty line at
+ // the end with moz-<br> or content <br> in a block level element.
+ if (lastFrame->GetType() == nsGkAtoms::brFrame) {
+ relativeRect = GetLineBreakerRectBefore(lastFrame);
+ }
+ // If there is a text frame at the end, use its information.
+ else if (lastFrame->GetType() == nsGkAtoms::textFrame) {
+ relativeRect = GuessLineBreakerRectAfter(lastFrame->GetContent());
+ }
+ // If there is an empty frame which is neither a text frame nor a <br>
+ // frame at the end, guess caret rect in it.
+ else {
+ relativeRect = GuessFirstCaretRectIn(lastFrame);
+ }
+ if (NS_WARN_IF(!relativeRect.IsValid())) {
+ return NS_ERROR_FAILURE;
+ }
+ rect = relativeRect.RectRelativeTo(lastFrame);
+ rv = ConvertToRootRelativeOffset(lastFrame, rect);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ aEvent->mReply.mWritingMode = lastFrame->GetWritingMode();
+ }
+ // Otherwise, if there are no contents in mRootContent, guess caret rect in
+ // its frame (with its font height and content box).
+ else {
+ nsIFrame* rootContentFrame = mRootContent->GetPrimaryFrame();
+ if (NS_WARN_IF(!rootContentFrame)) {
+ return NS_ERROR_FAILURE;
+ }
+ FrameRelativeRect relativeRect = GuessFirstCaretRectIn(rootContentFrame);
+ if (NS_WARN_IF(!relativeRect.IsValid())) {
+ return NS_ERROR_FAILURE;
+ }
+ rect = relativeRect.RectRelativeTo(rootContentFrame);
+ rv = ConvertToRootRelativeOffset(rootContentFrame, rect);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ aEvent->mReply.mWritingMode = rootContentFrame->GetWritingMode();
+ }
+ aEvent->mReply.mRect = LayoutDeviceIntRect::FromUnknownRect(
+ rect.ToOutsidePixels(mPresContext->AppUnitsPerDevPixel()));
+ EnsureNonEmptyRect(aEvent->mReply.mRect);
+ aEvent->mSucceeded = true;
+ return NS_OK;
}
nsRect rect, frameRect;
nsPoint ptOffset;
// If the first frame is a text frame, the result should be computed with
// the frame's rect but not including the rect before start point of the
// queried range.
--- a/dom/events/ContentEventHandler.h
+++ b/dom/events/ContentEventHandler.h
@@ -235,16 +235,24 @@ protected:
LineBreakType aLineBreakType,
uint32_t aMaxLength = UINT32_MAX);
// Get the text length of a given range of a content node in
// the given line break type.
static uint32_t GetTextLengthInRange(nsIContent* aContent,
uint32_t aXPStartOffset,
uint32_t aXPEndOffset,
LineBreakType aLineBreakType);
+ // Get the contents in aContent (meaning all children of aContent) as plain
+ // text. E.g., specifying mRootContent gets whole text in it.
+ // Note that the result is not same as .textContent. The result is
+ // optimized for native IMEs. For example, <br> element and some block
+ // elements causes "\n" (or "\r\n"), see also ShouldBreakLineBefore().
+ nsresult GenerateFlatTextContent(nsIContent* aContent,
+ nsAFlatString& aString,
+ LineBreakType aLineBreakType);
// Get the contents of aRange as plain text.
nsresult GenerateFlatTextContent(nsRange* aRange,
nsAFlatString& aString,
LineBreakType aLineBreakType);
// Get the text length before the start position of aRange.
nsresult GetFlatTextLengthBefore(nsRange* aRange,
uint32_t* aOffset,
LineBreakType aLineBreakType);
@@ -390,16 +398,31 @@ protected:
// immediately after aTextContent. This is useful when following block
// element causes a line break before it and it needs to compute the line
// breaker's rect. For example, if there is |<p>abc</p><p>def</p>|, the
// rect of 2nd <p>'s line breaker should be at right of "c" in the first
// <p>, not the start of 2nd <p>. The result is relative to the last text
// frame which represents the last character of aTextContent.
FrameRelativeRect GuessLineBreakerRectAfter(nsIContent* aTextContent);
+ // Returns a guessed first rect. I.e., it may be different from actual
+ // caret when selection is collapsed at start of aFrame. For example, this
+ // guess the caret rect only with the content box of aFrame and its font
+ // height like:
+ // +-aFrame----------------- (border box)
+ // |
+ // | +--------------------- (content box)
+ // | | I
+ // ^ guessed caret rect
+ // However, actual caret is computed with more information like line-height,
+ // child frames of aFrame etc. But this does not emulate actual caret
+ // behavior exactly for simpler and faster code because it's difficult and
+ // we're not sure it's worthwhile to do it with complicated implementation.
+ FrameRelativeRect GuessFirstCaretRectIn(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;
};