317 lines
12 KiB
C++
317 lines
12 KiB
C++
/*
|
|
* Copyright (C) 2020 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "config.h"
|
|
#include "CaretRectComputation.h"
|
|
|
|
#include "Editing.h"
|
|
#include "LayoutIntegrationLineIterator.h"
|
|
#include "LayoutIntegrationRunIterator.h"
|
|
#include "RenderBlockFlow.h"
|
|
#include "RenderInline.h"
|
|
#include "RenderLineBreak.h"
|
|
#include "RenderSVGInlineText.h"
|
|
#include "RenderText.h"
|
|
|
|
namespace WebCore {
|
|
|
|
static LayoutRect computeCaretRectForEmptyElement(const RenderBoxModelObject& renderer, LayoutUnit width, LayoutUnit textIndentOffset, CaretRectMode caretRectMode)
|
|
{
|
|
ASSERT(!renderer.firstChild());
|
|
|
|
// FIXME: This does not take into account either :first-line or :first-letter
|
|
// However, as soon as some content is entered, the line boxes will be
|
|
// constructed and this kludge is not called any more. So only the caret size
|
|
// of an empty :first-line'd block is wrong. I think we can live with that.
|
|
const RenderStyle& currentStyle = renderer.firstLineStyle();
|
|
|
|
enum CaretAlignment { AlignLeft, AlignRight, AlignCenter };
|
|
|
|
CaretAlignment alignment = AlignLeft;
|
|
|
|
switch (currentStyle.textAlign()) {
|
|
case TextAlignMode::Left:
|
|
case TextAlignMode::WebKitLeft:
|
|
break;
|
|
case TextAlignMode::Center:
|
|
case TextAlignMode::WebKitCenter:
|
|
alignment = AlignCenter;
|
|
break;
|
|
case TextAlignMode::Right:
|
|
case TextAlignMode::WebKitRight:
|
|
alignment = AlignRight;
|
|
break;
|
|
case TextAlignMode::Justify:
|
|
case TextAlignMode::Start:
|
|
if (!currentStyle.isLeftToRightDirection())
|
|
alignment = AlignRight;
|
|
break;
|
|
case TextAlignMode::End:
|
|
if (currentStyle.isLeftToRightDirection())
|
|
alignment = AlignRight;
|
|
break;
|
|
}
|
|
|
|
LayoutUnit x = renderer.borderLeft() + renderer.paddingLeft();
|
|
LayoutUnit maxX = width - renderer.borderRight() - renderer.paddingRight();
|
|
|
|
switch (alignment) {
|
|
case AlignLeft:
|
|
if (currentStyle.isLeftToRightDirection())
|
|
x += textIndentOffset;
|
|
break;
|
|
case AlignCenter:
|
|
x = (x + maxX) / 2;
|
|
if (currentStyle.isLeftToRightDirection())
|
|
x += textIndentOffset / 2;
|
|
else
|
|
x -= textIndentOffset / 2;
|
|
break;
|
|
case AlignRight:
|
|
x = maxX - caretWidth;
|
|
if (!currentStyle.isLeftToRightDirection())
|
|
x -= textIndentOffset;
|
|
break;
|
|
}
|
|
x = std::min(x, std::max<LayoutUnit>(maxX - caretWidth, 0));
|
|
|
|
auto lineHeight = renderer.lineHeight(true, currentStyle.isHorizontalWritingMode() ? HorizontalLine : VerticalLine, PositionOfInteriorLineBoxes);
|
|
auto height = std::min(lineHeight, LayoutUnit { currentStyle.fontMetrics().height() });
|
|
auto y = renderer.paddingTop() + renderer.borderTop() + (lineHeight > height ? (lineHeight - height) / 2 : LayoutUnit { });
|
|
|
|
auto rect = LayoutRect(x, y, caretWidth, height);
|
|
|
|
if (caretRectMode == CaretRectMode::ExpandToEndOfLine)
|
|
rect.shiftMaxXEdgeTo(width);
|
|
|
|
return currentStyle.isHorizontalWritingMode() ? rect : rect.transposedRect();
|
|
}
|
|
|
|
static LayoutRect computeCaretRectForLinePosition(const LayoutIntegration::LineIterator& line, float logicalLeftPosition, CaretRectMode caretRectMode)
|
|
{
|
|
auto& containingBlock = line->containingBlock();
|
|
auto lineSelectionRect = line->selectionRect();
|
|
|
|
int height = lineSelectionRect.height();
|
|
int top = lineSelectionRect.y();
|
|
|
|
// Distribute the caret's width to either side of the offset.
|
|
float left = logicalLeftPosition;
|
|
int caretWidthLeftOfOffset = caretWidth / 2;
|
|
left -= caretWidthLeftOfOffset;
|
|
int caretWidthRightOfOffset = caretWidth - caretWidthLeftOfOffset;
|
|
left = roundf(left);
|
|
|
|
float lineLeft = lineSelectionRect.x();
|
|
float lineRight = lineSelectionRect.maxX();
|
|
|
|
bool rightAligned = false;
|
|
switch (containingBlock.style().textAlign()) {
|
|
case TextAlignMode::Right:
|
|
case TextAlignMode::WebKitRight:
|
|
rightAligned = true;
|
|
break;
|
|
case TextAlignMode::Left:
|
|
case TextAlignMode::WebKitLeft:
|
|
case TextAlignMode::Center:
|
|
case TextAlignMode::WebKitCenter:
|
|
break;
|
|
case TextAlignMode::Justify:
|
|
case TextAlignMode::Start:
|
|
rightAligned = !containingBlock.style().isLeftToRightDirection();
|
|
break;
|
|
case TextAlignMode::End:
|
|
rightAligned = containingBlock.style().isLeftToRightDirection();
|
|
break;
|
|
}
|
|
|
|
float leftEdge = std::min<float>(0, lineLeft);
|
|
float rightEdge = std::max<float>(containingBlock.logicalWidth(), lineRight);
|
|
|
|
if (rightAligned) {
|
|
left = std::max(left, leftEdge);
|
|
left = std::min(left, lineRight - caretWidth);
|
|
} else {
|
|
left = std::min(left, rightEdge - caretWidthRightOfOffset);
|
|
left = std::max(left, lineLeft);
|
|
}
|
|
|
|
auto rect = IntRect(left, top, caretWidth, height);
|
|
|
|
if (caretRectMode == CaretRectMode::ExpandToEndOfLine)
|
|
rect.shiftMaxXEdgeTo(lineRight);
|
|
|
|
return containingBlock.style().isHorizontalWritingMode() ? rect : rect.transposedRect();
|
|
}
|
|
|
|
static LayoutRect computeCaretRectForText(const InlineRunAndOffset& runAndOffset, CaretRectMode caretRectMode)
|
|
{
|
|
if (!runAndOffset.run)
|
|
return { };
|
|
|
|
auto& textRun = downcast<LayoutIntegration::TextRunIterator>(runAndOffset.run);
|
|
auto line = textRun.line();
|
|
|
|
float position = textRun->positionForOffset(runAndOffset.offset);
|
|
return computeCaretRectForLinePosition(line, position, caretRectMode);
|
|
}
|
|
|
|
static LayoutRect computeCaretRectForLineBreak(const InlineRunAndOffset& runAndOffset, CaretRectMode caretRectMode)
|
|
{
|
|
ASSERT(!runAndOffset.offset);
|
|
|
|
if (!runAndOffset.run)
|
|
return { };
|
|
|
|
auto line = runAndOffset.run.line();
|
|
return computeCaretRectForLinePosition(line, line->contentLogicalLeft(), caretRectMode);
|
|
}
|
|
|
|
static LayoutRect computeCaretRectForSVGInlineText(const InlineRunAndOffset& runAndOffset, CaretRectMode)
|
|
{
|
|
auto* box = runAndOffset.run ? runAndOffset.run->legacyInlineBox() : nullptr;
|
|
auto caretOffset = runAndOffset.offset;
|
|
|
|
if (!is<LegacyInlineTextBox>(box))
|
|
return { };
|
|
|
|
auto& textBox = downcast<LegacyInlineTextBox>(*box);
|
|
if (caretOffset < textBox.start() || caretOffset > textBox.start() + textBox.len())
|
|
return { };
|
|
|
|
// Use the edge of the selection rect to determine the caret rect.
|
|
if (caretOffset < textBox.start() + textBox.len()) {
|
|
LayoutRect rect = textBox.localSelectionRect(caretOffset, caretOffset + 1);
|
|
LayoutUnit x = textBox.isLeftToRightDirection() ? rect.x() : rect.maxX();
|
|
return LayoutRect(x, rect.y(), caretWidth, rect.height());
|
|
}
|
|
|
|
LayoutRect rect = textBox.localSelectionRect(caretOffset - 1, caretOffset);
|
|
LayoutUnit x = textBox.isLeftToRightDirection() ? rect.maxX() : rect.x();
|
|
return { x, rect.y(), caretWidth, rect.height() };
|
|
}
|
|
|
|
static LayoutRect computeCaretRectForBox(const RenderBox& renderer, const InlineRunAndOffset& runAndOffset, CaretRectMode caretRectMode)
|
|
{
|
|
// VisiblePositions at offsets inside containers either a) refer to the positions before/after
|
|
// those containers (tables and select elements) or b) refer to the position inside an empty block.
|
|
// They never refer to children.
|
|
// FIXME: Paint the carets inside empty blocks differently than the carets before/after elements.
|
|
|
|
LayoutRect rect(renderer.location(), LayoutSize(caretWidth, renderer.height()));
|
|
bool ltr = runAndOffset.run ? runAndOffset.run->isLeftToRightDirection() : renderer.style().isLeftToRightDirection();
|
|
|
|
if ((!runAndOffset.offset) ^ ltr)
|
|
rect.move(LayoutSize(renderer.width() - caretWidth, 0_lu));
|
|
|
|
if (runAndOffset.run) {
|
|
auto line = runAndOffset.run.line();
|
|
LayoutUnit top = line->top();
|
|
rect.setY(top);
|
|
rect.setHeight(line->bottom() - top);
|
|
}
|
|
|
|
// If height of box is smaller than font height, use the latter one,
|
|
// otherwise the caret might become invisible.
|
|
//
|
|
// Also, if the box is not a replaced element, always use the font height.
|
|
// This prevents the "big caret" bug described in:
|
|
// <rdar://problem/3777804> Deleting all content in a document can result in giant tall-as-window insertion point
|
|
//
|
|
// FIXME: ignoring :first-line, missing good reason to take care of
|
|
LayoutUnit fontHeight = renderer.style().fontMetrics().height();
|
|
if (fontHeight > rect.height() || (!renderer.isReplaced() && !renderer.isTable()))
|
|
rect.setHeight(fontHeight);
|
|
|
|
// Move to local coords
|
|
rect.moveBy(-renderer.location());
|
|
|
|
// FIXME: Border/padding should be added for all elements but this workaround
|
|
// is needed because we use offsets inside an "atomic" element to represent
|
|
// positions before and after the element in deprecated editing offsets.
|
|
if (renderer.element() && !(editingIgnoresContent(*renderer.element()) || isRenderedTable(renderer.element()))) {
|
|
rect.setX(rect.x() + renderer.borderLeft() + renderer.paddingLeft());
|
|
rect.setY(rect.y() + renderer.paddingTop() + renderer.borderTop());
|
|
}
|
|
|
|
if (caretRectMode == CaretRectMode::ExpandToEndOfLine)
|
|
rect.shiftMaxXEdgeTo(renderer.x() + renderer.width());
|
|
|
|
return renderer.isHorizontalWritingMode() ? rect : rect.transposedRect();
|
|
}
|
|
|
|
static LayoutRect computeCaretRectForBlock(const RenderBlock& renderer, const InlineRunAndOffset& runAndOffset, CaretRectMode caretRectMode)
|
|
{
|
|
// Do the normal calculation in most cases.
|
|
if (renderer.firstChild())
|
|
return computeCaretRectForBox(renderer, runAndOffset, caretRectMode);
|
|
|
|
return computeCaretRectForEmptyElement(renderer, renderer.width(), renderer.textIndentOffset(), caretRectMode);
|
|
}
|
|
|
|
static LayoutRect computeCaretRectForInline(const RenderInline& renderer)
|
|
{
|
|
if (renderer.firstChild()) {
|
|
// This condition is possible if the RenderInline is at an editing boundary,
|
|
// i.e. the VisiblePosition is:
|
|
// <RenderInline editingBoundary=true>|<RenderText> </RenderText></RenderInline>
|
|
// FIXME: need to figure out how to make this return a valid rect, note that
|
|
// there are no line boxes created in the above case.
|
|
return { };
|
|
}
|
|
|
|
LayoutRect caretRect = computeCaretRectForEmptyElement(renderer, renderer.horizontalBorderAndPaddingExtent(), 0, CaretRectMode::Normal);
|
|
|
|
if (LegacyInlineBox* firstBox = renderer.firstLineBox())
|
|
caretRect.moveBy(LayoutPoint(firstBox->topLeft()));
|
|
|
|
return caretRect;
|
|
}
|
|
|
|
LayoutRect computeLocalCaretRect(const RenderObject& renderer, const InlineRunAndOffset& runAndOffset, CaretRectMode caretRectMode)
|
|
{
|
|
if (is<RenderSVGInlineText>(renderer))
|
|
return computeCaretRectForSVGInlineText(runAndOffset, caretRectMode);
|
|
|
|
if (is<RenderText>(renderer))
|
|
return computeCaretRectForText(runAndOffset, caretRectMode);
|
|
|
|
if (is<RenderLineBreak>(renderer))
|
|
return computeCaretRectForLineBreak(runAndOffset, caretRectMode);
|
|
|
|
if (is<RenderBlock>(renderer))
|
|
return computeCaretRectForBlock(downcast<RenderBlock>(renderer), runAndOffset, caretRectMode);
|
|
|
|
if (is<RenderBox>(renderer))
|
|
return computeCaretRectForBox(downcast<RenderBox>(renderer), runAndOffset, caretRectMode);
|
|
|
|
if (is<RenderInline>(renderer))
|
|
return computeCaretRectForInline(downcast<RenderInline>(renderer));
|
|
|
|
return { };
|
|
}
|
|
|
|
};
|