Skip to content

Commit

Permalink
Bug 1286464 part.19 ContentEventHandler::OnQueryTextRect() should han…
Browse files Browse the repository at this point in the history
…dle 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 mozilla#3.

MozReview-Commit-ID: FS9cWJQ39DK
  • Loading branch information
masayuki-nakano committed Aug 12, 2016
1 parent ea6f234 commit be9035a
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 9 deletions.
163 changes: 154 additions & 9 deletions dom/events/ContentEventHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -663,12 +663,28 @@ ContentEventHandler::ShouldBreakLineBefore(nsIContent* 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;
Expand Down Expand Up @@ -1722,6 +1738,49 @@ ContentEventHandler::GuessLineBreakerRectAfter(nsIContent* aTextContent)
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)
{
Expand Down Expand Up @@ -1758,10 +1817,23 @@ ContentEventHandler::OnQueryTextRectArray(WidgetQueryContentEvent* aEvent)
// 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;
}

Expand Down Expand Up @@ -2045,12 +2117,85 @@ ContentEventHandler::OnQueryTextRect(WidgetQueryContentEvent* aEvent)
// 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;
Expand Down
23 changes: 23 additions & 0 deletions dom/events/ContentEventHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,14 @@ class MOZ_STACK_CLASS ContentEventHandler
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,
Expand Down Expand Up @@ -395,6 +403,21 @@ class MOZ_STACK_CLASS ContentEventHandler
// 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
Expand Down

0 comments on commit be9035a

Please sign in to comment.