blob: 18a1ffdadd06e779719245257a3684c4dacddb75 [file] [log] [blame]
/*
* Copyright (C) 2021 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 "InlineDisplayContentBuilder.h"
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
#include "FontCascade.h"
#include "InlineTextBoxStyle.h"
#include "LayoutBoxGeometry.h"
#include "LayoutInitialContainingBlock.h"
#include "TextUtil.h"
#include <wtf/ListHashSet.h>
#include <wtf/Range.h>
using WTF::Range;
namespace WebCore {
namespace Layout {
static inline LayoutUnit marginLeftInInlineDirection(const Layout::BoxGeometry& boxGeometry, bool isLeftToRightDirection)
{
return isLeftToRightDirection ? boxGeometry.marginStart() : boxGeometry.marginEnd();
}
static inline LayoutUnit marginRightInInlineDirection(const Layout::BoxGeometry& boxGeometry, bool isLeftToRightDirection)
{
return isLeftToRightDirection ? boxGeometry.marginEnd() : boxGeometry.marginStart();
}
static inline LayoutUnit borderLeftInInlineDirection(const Layout::BoxGeometry& boxGeometry, bool isLeftToRightDirection)
{
return isLeftToRightDirection ? boxGeometry.borderStart() : boxGeometry.borderEnd();
}
static inline LayoutUnit borderRightInInlineDirection(const Layout::BoxGeometry& boxGeometry, bool isLeftToRightDirection)
{
return isLeftToRightDirection ? boxGeometry.borderEnd() : boxGeometry.borderStart();
}
static inline LayoutUnit paddingLeftInInlineDirection(const Layout::BoxGeometry& boxGeometry, bool isLeftToRightDirection)
{
return isLeftToRightDirection ? boxGeometry.paddingStart().value_or(0_lu) : boxGeometry.paddingEnd().value_or(0_lu);
}
static inline LayoutUnit paddingRightInInlineDirection(const Layout::BoxGeometry& boxGeometry, bool isLeftToRightDirection)
{
return isLeftToRightDirection ? boxGeometry.paddingEnd().value_or(0_lu) : boxGeometry.paddingStart().value_or(0_lu);
}
static inline OptionSet<InlineDisplay::Box::PositionWithinInlineLevelBox> isFirstLastBox(const InlineLevelBox& inlineBox)
{
auto positionWithinInlineLevelBox = OptionSet<InlineDisplay::Box::PositionWithinInlineLevelBox> { };
if (inlineBox.isFirstBox())
positionWithinInlineLevelBox.add(InlineDisplay::Box::PositionWithinInlineLevelBox::First);
if (inlineBox.isLastBox())
positionWithinInlineLevelBox.add(InlineDisplay::Box::PositionWithinInlineLevelBox::Last);
return positionWithinInlineLevelBox;
}
InlineDisplayContentBuilder::InlineDisplayContentBuilder(const ContainerBox& formattingContextRoot, InlineFormattingState& formattingState)
: m_formattingContextRoot(formattingContextRoot)
, m_formattingState(formattingState)
{
}
DisplayBoxes InlineDisplayContentBuilder::build(const LineBuilder::LineContent& lineContent, const LineBox& lineBox, const InlineDisplay::Line& displayLine, const size_t lineIndex)
{
DisplayBoxes boxes;
boxes.reserveInitialCapacity(lineContent.runs.size() + lineBox.nonRootInlineLevelBoxes().size() + 1);
m_lineIndex = lineIndex;
// Every line starts with a root box, even the empty ones.
auto rootInlineBoxRect = flipRootInlineBoxRectToVisualForWritingMode(lineBox.logicalRectForRootInlineBox(), displayLine, root().style().writingMode());
boxes.append({ m_lineIndex, InlineDisplay::Box::Type::RootInlineBox, root(), UBIDI_DEFAULT_LTR, rootInlineBoxRect, rootInlineBoxRect, { }, { }, lineBox.rootInlineBox().hasContent() });
auto contentNeedsBidiReordering = !lineContent.visualOrderList.isEmpty();
if (contentNeedsBidiReordering)
processBidiContent(lineContent, lineBox, displayLine, boxes);
else
processNonBidiContent(lineContent, lineBox, displayLine, boxes);
processOverflownRunsForEllipsis(boxes, displayLine.right());
collectInkOverflowForTextDecorations(boxes, displayLine);
collectInkOverflowForInlineBoxes(boxes);
return boxes;
}
static inline bool computeInkOverflowForInlineLevelBox(const RenderStyle& style, FloatRect& inkOverflow)
{
auto hasVisualOverflow = false;
auto inflateWithOutline = [&] {
if (!style.hasOutlineInVisualOverflow())
return;
inkOverflow.inflate(style.outlineSize());
hasVisualOverflow = true;
};
inflateWithOutline();
auto inflateWithBoxShadow = [&] {
auto topBoxShadow = LayoutUnit { };
auto bottomBoxShadow = LayoutUnit { };
style.getBoxShadowBlockDirectionExtent(topBoxShadow, bottomBoxShadow);
auto leftBoxShadow = LayoutUnit { };
auto rightBoxShadow = LayoutUnit { };
style.getBoxShadowInlineDirectionExtent(leftBoxShadow, rightBoxShadow);
if (!topBoxShadow && !bottomBoxShadow && !leftBoxShadow && !rightBoxShadow)
return;
inkOverflow.inflate(leftBoxShadow.toFloat(), topBoxShadow.toFloat(), rightBoxShadow.toFloat(), bottomBoxShadow.toFloat());
hasVisualOverflow = true;
};
inflateWithBoxShadow();
return hasVisualOverflow;
}
void InlineDisplayContentBuilder::appendTextDisplayBox(const Line::Run& lineRun, const InlineRect& textRunRect, DisplayBoxes& boxes)
{
ASSERT(lineRun.textContent() && is<InlineTextBox>(lineRun.layoutBox()));
auto& layoutBox = lineRun.layoutBox();
auto& style = !m_lineIndex ? layoutBox.firstLineStyle() : layoutBox.style();
auto inkOverflow = [&] {
auto initialContaingBlockSize = formattingState().layoutState().isInlineFormattingContextIntegration()
? formattingState().layoutState().viewportSize()
: formattingState().layoutState().geometryForBox(layoutBox.initialContainingBlock()).contentBox().size();
auto strokeOverflow = ceilf(style.computedStrokeWidth(ceiledIntSize(initialContaingBlockSize)));
auto inkOverflow = textRunRect;
inkOverflow.inflate(strokeOverflow);
auto letterSpacing = style.fontCascade().letterSpacing();
if (letterSpacing < 0) {
// Last letter's negative spacing shrinks logical rect. Push it to ink overflow.
inkOverflow.expand(-letterSpacing, { });
}
auto textShadow = style.textShadowExtent();
inkOverflow.inflate(-textShadow.top(), textShadow.right(), textShadow.bottom(), -textShadow.left());
return inkOverflow;
};
auto& content = downcast<InlineTextBox>(layoutBox).content();
auto& text = lineRun.textContent();
auto adjustedContentToRender = [&] {
return text->needsHyphen ? makeString(StringView(content).substring(text->start, text->length), style.hyphenString()) : String();
};
boxes.append({ m_lineIndex
, lineRun.isWordSeparator() ? InlineDisplay::Box::Type::WordSeparator : InlineDisplay::Box::Type::Text
, layoutBox
, lineRun.bidiLevel()
, textRunRect
, inkOverflow()
, lineRun.expansion()
, InlineDisplay::Box::Text { text->start, text->length, content, adjustedContentToRender(), text->needsHyphen }
, true
, { }
});
}
void InlineDisplayContentBuilder::appendSoftLineBreakDisplayBox(const Line::Run& lineRun, const InlineRect& softLineBreakRunRect, DisplayBoxes& boxes)
{
ASSERT(lineRun.textContent() && is<InlineTextBox>(lineRun.layoutBox()));
auto& layoutBox = lineRun.layoutBox();
auto& text = lineRun.textContent();
boxes.append({ m_lineIndex
, InlineDisplay::Box::Type::SoftLineBreak
, layoutBox
, lineRun.bidiLevel()
, softLineBreakRunRect
, softLineBreakRunRect
, lineRun.expansion()
, InlineDisplay::Box::Text { text->start, text->length, downcast<InlineTextBox>(layoutBox).content() }
});
}
void InlineDisplayContentBuilder::appendHardLineBreakDisplayBox(const Line::Run& lineRun, const InlineRect& lineBreakBoxRect, DisplayBoxes& boxes)
{
auto& layoutBox = lineRun.layoutBox();
boxes.append({ m_lineIndex
, InlineDisplay::Box::Type::LineBreakBox
, layoutBox
, lineRun.bidiLevel()
, lineBreakBoxRect
, lineBreakBoxRect
, lineRun.expansion()
, { }
});
auto& boxGeometry = formattingState().boxGeometry(layoutBox);
boxGeometry.setLogicalTopLeft(toLayoutPoint(lineBreakBoxRect.topLeft()));
boxGeometry.setContentBoxHeight(toLayoutUnit(lineBreakBoxRect.height()));
}
void InlineDisplayContentBuilder::appendAtomicInlineLevelDisplayBox(const Line::Run& lineRun, const InlineRect& borderBoxRect, DisplayBoxes& boxes)
{
ASSERT(lineRun.layoutBox().isAtomicInlineLevelBox());
auto& layoutBox = lineRun.layoutBox();
auto inkOverflow = [&] {
auto inkOverflow = FloatRect { borderBoxRect };
computeInkOverflowForInlineLevelBox(!m_lineIndex ? layoutBox.firstLineStyle() : layoutBox.style(), inkOverflow);
// Atomic inline box contribute to their inline box parents ink overflow at all times (e.g. <span><img></span>).
m_contentHasInkOverflow = m_contentHasInkOverflow || &layoutBox.parent() != &root();
return inkOverflow;
};
boxes.append({ m_lineIndex
, InlineDisplay::Box::Type::AtomicInlineLevelBox
, layoutBox
, lineRun.bidiLevel()
, borderBoxRect
, inkOverflow()
, lineRun.expansion()
, { }
});
// Note that inline boxes are relative to the line and their top position can be negative.
// Atomic inline boxes are all set. Their margin/border/content box geometries are already computed. We just have to position them here.
formattingState().boxGeometry(layoutBox).setLogicalTopLeft(toLayoutPoint(borderBoxRect.topLeft()));
}
void InlineDisplayContentBuilder::setInlineBoxGeometry(const Box& layoutBox, const InlineRect& rect, bool isFirstInlineBoxFragment)
{
auto adjustedSize = LayoutSize { LayoutUnit::fromFloatCeil(rect.width()), LayoutUnit::fromFloatCeil(rect.height()) };
auto adjustedRect = Rect { LayoutPoint { rect.topLeft() }, adjustedSize };
auto& boxGeometry = formattingState().boxGeometry(layoutBox);
if (!isFirstInlineBoxFragment) {
auto enclosingBorderBoxRect = BoxGeometry::borderBoxRect(boxGeometry);
enclosingBorderBoxRect.expandToContain(adjustedRect);
adjustedRect = enclosingBorderBoxRect;
}
boxGeometry.setLogicalTopLeft(adjustedRect.topLeft());
auto contentBoxHeight = adjustedRect.height() - (boxGeometry.verticalBorder() + boxGeometry.verticalPadding().value_or(0_lu));
auto contentBoxWidth = adjustedRect.width() - (boxGeometry.horizontalBorder() + boxGeometry.horizontalPadding().value_or(0_lu));
boxGeometry.setContentBoxHeight(contentBoxHeight);
boxGeometry.setContentBoxWidth(contentBoxWidth);
}
void InlineDisplayContentBuilder::appendInlineBoxDisplayBox(const Line::Run& lineRun, const InlineLevelBox& inlineBox, const InlineRect& inlineBoxBorderBox, bool linehasContent, DisplayBoxes& boxes)
{
ASSERT(lineRun.layoutBox().isInlineBox());
auto& layoutBox = lineRun.layoutBox();
if (!linehasContent) {
// FIXME: It's expected to not have any boxes on empty lines. We should reconsider this.
setInlineBoxGeometry(layoutBox, inlineBoxBorderBox, true);
return;
}
auto inkOverflow = [&] {
auto inkOverflow = FloatRect { inlineBoxBorderBox };
m_contentHasInkOverflow = computeInkOverflowForInlineLevelBox(!m_lineIndex ? layoutBox.firstLineStyle() : layoutBox.style(), inkOverflow) || m_contentHasInkOverflow;
return inkOverflow;
};
ASSERT(inlineBox.isInlineBox());
ASSERT(inlineBox.isFirstBox());
boxes.append({ m_lineIndex
, InlineDisplay::Box::Type::NonRootInlineBox
, layoutBox
, lineRun.bidiLevel()
, inlineBoxBorderBox
, inkOverflow()
, { }
, { }
, inlineBox.hasContent()
, isFirstLastBox(inlineBox)
});
// This inline box showed up first on this line.
setInlineBoxGeometry(layoutBox, inlineBoxBorderBox, true);
}
void InlineDisplayContentBuilder::appendSpanningInlineBoxDisplayBox(const Line::Run& lineRun, const InlineLevelBox& inlineBox, const InlineRect& inlineBoxBorderBox, DisplayBoxes& boxes)
{
ASSERT(lineRun.layoutBox().isInlineBox());
auto& layoutBox = lineRun.layoutBox();
auto inkOverflow = [&] {
auto inkOverflow = FloatRect { inlineBoxBorderBox };
m_contentHasInkOverflow = computeInkOverflowForInlineLevelBox(!m_lineIndex ? layoutBox.firstLineStyle() : layoutBox.style(), inkOverflow) || m_contentHasInkOverflow;
return inkOverflow;
};
ASSERT(!inlineBox.isFirstBox());
boxes.append({ m_lineIndex
, InlineDisplay::Box::Type::NonRootInlineBox
, layoutBox
, lineRun.bidiLevel()
, inlineBoxBorderBox
, inkOverflow()
, { }
, { }
, inlineBox.hasContent()
, isFirstLastBox(inlineBox)
});
// Middle or end of the inline box. Let's stretch the box as needed.
setInlineBoxGeometry(layoutBox, inlineBoxBorderBox, false);
}
void InlineDisplayContentBuilder::appendInlineDisplayBoxAtBidiBoundary(const Box& layoutBox, DisplayBoxes& boxes)
{
// Geometries for inline boxes at bidi boundaries are computed at a post-process step.
boxes.append({ m_lineIndex
, InlineDisplay::Box::Type::NonRootInlineBox
, layoutBox
, UBIDI_DEFAULT_LTR
, { }
, { }
, { }
, { }
});
}
void InlineDisplayContentBuilder::processNonBidiContent(const LineBuilder::LineContent& lineContent, const LineBox& lineBox, const InlineDisplay::Line& displayLine, DisplayBoxes& boxes)
{
#ifndef NDEBUG
auto hasContent = false;
for (auto& lineRun : lineContent.runs)
hasContent = hasContent || lineRun.isText() || lineRun.isBox();
ASSERT(lineContent.inlineBaseDirection == TextDirection::LTR || !hasContent);
#endif
auto writingMode = root().style().writingMode();
auto contentStartInVisualOrder = movePointHorizontallyForWritingMode(displayLine.topLeft(), displayLine.contentLogicalOffset(), writingMode);
for (auto& lineRun : lineContent.runs) {
auto& layoutBox = lineRun.layoutBox();
auto visualRectRelativeToRoot = [&](auto logicalRect) {
auto isContentRun = !lineRun.isInlineBoxStart() && !lineRun.isInlineBoxEnd() && !lineRun.isLineSpanningInlineBoxStart();
auto visualRect = isContentRun ? flipLogicalRectToVisualForWritingModeWithinLine(logicalRect, lineBox.logicalRect(), writingMode)
: flipLogicalRectToVisualForWritingMode(logicalRect, writingMode);
visualRect.moveBy(contentStartInVisualOrder);
return visualRect;
};
if (lineRun.isText()) {
appendTextDisplayBox(lineRun, visualRectRelativeToRoot(lineBox.logicalRectForTextRun(lineRun)), boxes);
continue;
}
if (lineRun.isSoftLineBreak()) {
appendSoftLineBreakDisplayBox(lineRun, visualRectRelativeToRoot(lineBox.logicalRectForTextRun(lineRun)), boxes);
continue;
}
if (lineRun.isHardLineBreak()) {
appendHardLineBreakDisplayBox(lineRun, visualRectRelativeToRoot(lineBox.logicalRectForLineBreakBox(layoutBox)), boxes);
continue;
}
if (lineRun.isBox() || lineRun.isListMarker()) {
appendAtomicInlineLevelDisplayBox(lineRun
, visualRectRelativeToRoot(lineBox.logicalBorderBoxForAtomicInlineLevelBox(layoutBox, formattingState().boxGeometry(layoutBox)))
, boxes);
continue;
}
if (lineRun.isInlineBoxStart()) {
appendInlineBoxDisplayBox(lineRun
, lineBox.inlineLevelBoxForLayoutBox(lineRun.layoutBox())
, visualRectRelativeToRoot(lineBox.logicalBorderBoxForInlineBox(layoutBox, formattingState().boxGeometry(layoutBox)))
, lineBox.hasContent()
, boxes);
continue;
}
if (lineRun.isLineSpanningInlineBoxStart()) {
if (!lineBox.hasContent()) {
// When a spanning inline box (e.g. <div>text<span><br></span></div>) lands on an empty line
// (empty here means no content at all including line breaks, not just visually empty) then we
// don't extend the spanning line box over to this line -also there is no next line in cases like this.
continue;
}
appendSpanningInlineBoxDisplayBox(lineRun
, lineBox.inlineLevelBoxForLayoutBox(lineRun.layoutBox())
, visualRectRelativeToRoot(lineBox.logicalBorderBoxForInlineBox(layoutBox, formattingState().boxGeometry(layoutBox)))
, boxes);
continue;
}
ASSERT(lineRun.isInlineBoxEnd() || lineRun.isWordBreakOpportunity());
}
}
struct DisplayBoxTree {
public:
struct Node {
// Node's parent index in m_displayBoxNodes.
std::optional<size_t> parentIndex;
// Associated display box index in DisplayBoxes.
size_t displayBoxIndex { 0 };
// Child indexes in m_displayBoxNodes.
Vector<size_t> children { };
};
DisplayBoxTree()
{
m_displayBoxNodes.append({ });
}
const Node& root() const { return m_displayBoxNodes.first(); }
Node& at(size_t index) { return m_displayBoxNodes[index]; }
const Node& at(size_t index) const { return m_displayBoxNodes[index]; }
size_t append(size_t parentNodeIndex, size_t childDisplayBoxIndex)
{
auto childDisplayBoxNodeIndex = m_displayBoxNodes.size();
m_displayBoxNodes.append({ parentNodeIndex, childDisplayBoxIndex });
at(parentNodeIndex).children.append(childDisplayBoxNodeIndex);
return childDisplayBoxNodeIndex;
}
private:
Vector<Node, 10> m_displayBoxNodes;
};
struct AncestorStack {
std::optional<size_t> unwind(const ContainerBox& containerBox)
{
// Unwind the stack all the way to container box.
if (!m_set.contains(&containerBox))
return { };
while (m_set.last() != &containerBox) {
m_stack.removeLast();
m_set.removeLast();
}
// Root is always a common ancestor.
ASSERT(!m_stack.isEmpty());
return m_stack.last();
}
void push(size_t displayBoxNodeIndexForContainerBox, const ContainerBox& containerBox)
{
m_stack.append(displayBoxNodeIndexForContainerBox);
ASSERT(!m_set.contains(&containerBox));
m_set.add(&containerBox);
}
private:
Vector<size_t> m_stack;
ListHashSet<const ContainerBox*> m_set;
};
static inline size_t createdDisplayBoxNodeForContainerBoxAndPushToAncestorStack(const ContainerBox& containerBox, size_t displayBoxIndex, size_t parentDisplayBoxNodeIndex, DisplayBoxTree& displayBoxTree, AncestorStack& ancestorStack)
{
auto displayBoxNodeIndex = displayBoxTree.append(parentDisplayBoxNodeIndex, displayBoxIndex);
ancestorStack.push(displayBoxNodeIndex, containerBox);
return displayBoxNodeIndex;
}
size_t InlineDisplayContentBuilder::ensureDisplayBoxForContainer(const ContainerBox& containerBox, DisplayBoxTree& displayBoxTree, AncestorStack& ancestorStack, DisplayBoxes& boxes)
{
ASSERT(containerBox.isInlineBox() || &containerBox == &root());
if (auto lowestCommonAncestorIndex = ancestorStack.unwind(containerBox))
return *lowestCommonAncestorIndex;
auto enclosingDisplayBoxNodeIndexForContainer = ensureDisplayBoxForContainer(containerBox.parent(), displayBoxTree, ancestorStack, boxes);
appendInlineDisplayBoxAtBidiBoundary(containerBox, boxes);
return createdDisplayBoxNodeForContainerBoxAndPushToAncestorStack(containerBox, boxes.size() - 1, enclosingDisplayBoxNodeIndexForContainer, displayBoxTree, ancestorStack);
}
struct IsFirstLastIndex {
std::optional<size_t> first;
std::optional<size_t> last;
};
using IsFirstLastIndexesMap = HashMap<const Box*, IsFirstLastIndex>;
void InlineDisplayContentBuilder::adjustVisualGeometryForDisplayBox(size_t displayBoxNodeIndex, InlineLayoutUnit& contentRightInInlineDirectionVisualOrder, InlineLayoutUnit lineBoxLogicalTop, const DisplayBoxTree& displayBoxTree, DisplayBoxes& boxes, const LineBox& lineBox, const IsFirstLastIndexesMap& isFirstLastIndexesMap)
{
auto writingMode = root().style().writingMode();
auto isHorizontalWritingMode = WebCore::isHorizontalWritingMode(writingMode);
// Non-inline box display boxes just need a horizontal adjustment while
// inline box type of display boxes need
// 1. horizontal adjustment and margin/border/padding start offsetting on the first box
// 2. right edge computation including descendant content width and margin/border/padding end offsetting on the last box
auto& displayBox = boxes[displayBoxTree.at(displayBoxNodeIndex).displayBoxIndex];
auto& layoutBox = displayBox.layoutBox();
if (!displayBox.isNonRootInlineBox()) {
if (displayBox.isAtomicInlineLevelBox() || displayBox.isGenericInlineLevelBox()) {
auto isLeftToRightDirection = layoutBox.parent().style().isLeftToRightDirection();
auto& boxGeometry = formattingState().boxGeometry(layoutBox);
auto boxMarginLeft = marginLeftInInlineDirection(boxGeometry, isLeftToRightDirection);
auto borderBoxLeft = LayoutUnit { contentRightInInlineDirectionVisualOrder + boxMarginLeft };
boxGeometry.setLogicalLeft(borderBoxLeft);
setLeftForWritingMode(displayBox, borderBoxLeft, writingMode);
contentRightInInlineDirectionVisualOrder += boxGeometry.marginBoxWidth();
} else {
auto wordSpacingMargin = displayBox.isWordSeparator() ? layoutBox.style().fontCascade().wordSpacing() : 0.0f;
setLeftForWritingMode(displayBox, contentRightInInlineDirectionVisualOrder + wordSpacingMargin, writingMode);
contentRightInInlineDirectionVisualOrder += (isHorizontalWritingMode ? displayBox.width() : displayBox.height()) + wordSpacingMargin;
}
return;
}
auto& boxGeometry = formattingState().boxGeometry(layoutBox);
auto isLeftToRightDirection = layoutBox.style().isLeftToRightDirection();
auto isFirstLastIndexes = isFirstLastIndexesMap.get(&layoutBox);
auto isFirstBox = isFirstLastIndexes.first && *isFirstLastIndexes.first == displayBoxNodeIndex;
auto isLastBox = isFirstLastIndexes.last && *isFirstLastIndexes.last == displayBoxNodeIndex;
auto beforeInlineBoxContent = [&] {
auto logicalRect = lineBox.logicalBorderBoxForInlineBox(layoutBox, boxGeometry);
auto visualRect = flipLogicalRectToVisualForWritingMode({ lineBoxLogicalTop + logicalRect.top(), contentRightInInlineDirectionVisualOrder, { }, logicalRect.height() }, writingMode);
displayBox.setRect(visualRect, visualRect);
auto shouldApplyLeftSide = (isLeftToRightDirection && isFirstBox) || (!isLeftToRightDirection && isLastBox);
if (!shouldApplyLeftSide)
return;
contentRightInInlineDirectionVisualOrder += marginLeftInInlineDirection(boxGeometry, isLeftToRightDirection);
setLeftForWritingMode(displayBox, contentRightInInlineDirectionVisualOrder, writingMode);
contentRightInInlineDirectionVisualOrder += borderLeftInInlineDirection(boxGeometry, isLeftToRightDirection) + paddingLeftInInlineDirection(boxGeometry, isLeftToRightDirection);
};
beforeInlineBoxContent();
for (auto childDisplayBoxNodeIndex : displayBoxTree.at(displayBoxNodeIndex).children)
adjustVisualGeometryForDisplayBox(childDisplayBoxNodeIndex, contentRightInInlineDirectionVisualOrder, lineBoxLogicalTop, displayBoxTree, boxes, lineBox, isFirstLastIndexesMap);
auto afterInlineBoxContent = [&] {
auto shouldApplyRightSide = (isLeftToRightDirection && isLastBox) || (!isLeftToRightDirection && isFirstBox);
if (!shouldApplyRightSide)
return setRightForWritingMode(displayBox, contentRightInInlineDirectionVisualOrder, writingMode);
contentRightInInlineDirectionVisualOrder += borderRightInInlineDirection(boxGeometry, isLeftToRightDirection) + paddingRightInInlineDirection(boxGeometry, isLeftToRightDirection);
setRightForWritingMode(displayBox, contentRightInInlineDirectionVisualOrder, writingMode);
contentRightInInlineDirectionVisualOrder += marginRightInInlineDirection(boxGeometry, isLeftToRightDirection);
};
afterInlineBoxContent();
auto computeInkOverflow = [&] {
auto inkOverflow = FloatRect { displayBox.visualRectIgnoringBlockDirection() };
m_contentHasInkOverflow = computeInkOverflowForInlineLevelBox(!m_lineIndex ? layoutBox.firstLineStyle() : layoutBox.style(), inkOverflow) || m_contentHasInkOverflow;
displayBox.adjustInkOverflow(inkOverflow);
};
computeInkOverflow();
setInlineBoxGeometry(layoutBox, displayBox.visualRectIgnoringBlockDirection(), isFirstBox);
if (lineBox.inlineLevelBoxForLayoutBox(layoutBox).hasContent())
displayBox.setHasContent();
}
void InlineDisplayContentBuilder::processBidiContent(const LineBuilder::LineContent& lineContent, const LineBox& lineBox, const InlineDisplay::Line& displayLine, DisplayBoxes& boxes)
{
ASSERT(lineContent.visualOrderList.size() <= lineContent.runs.size());
AncestorStack ancestorStack;
auto displayBoxTree = DisplayBoxTree { };
ancestorStack.push({ }, root());
auto writingMode = root().style().writingMode();
auto isHorizontalWritingMode = WebCore::isHorizontalWritingMode(writingMode);
auto lineLogicalTop = isHorizontalWritingMode ? displayLine.top() : displayLine.left();
auto lineLogicalLeft = isHorizontalWritingMode ? displayLine.left() : displayLine.top();
auto contentStartInInlineDirectionVisualOrder = lineLogicalLeft + displayLine.contentLogicalOffset();
auto hasInlineBox = false;
auto createDisplayBoxesInVisualOrder = [&] {
auto contentRightInInlineDirectionVisualOrder = contentStartInInlineDirectionVisualOrder;
auto& runs = lineContent.runs;
for (auto visualOrder : lineContent.visualOrderList) {
ASSERT(runs[visualOrder].bidiLevel() != InlineItem::opaqueBidiLevel);
auto& lineRun = runs[visualOrder];
auto& layoutBox = lineRun.layoutBox();
auto needsDisplayBox = !lineRun.isWordBreakOpportunity() && !lineRun.isInlineBoxEnd();
if (!needsDisplayBox)
continue;
auto visualRectRelativeToRoot = [&](auto logicalRect) {
auto visualRect = flipLogicalRectToVisualForWritingModeWithinLine(logicalRect, lineBox.logicalRect(), writingMode);
if (isHorizontalWritingMode) {
visualRect.setLeft(contentRightInInlineDirectionVisualOrder);
visualRect.moveVertically(lineLogicalTop);
} else {
visualRect.setTop(contentRightInInlineDirectionVisualOrder);
visualRect.moveHorizontally(lineLogicalTop);
}
return visualRect;
};
auto parentDisplayBoxNodeIndex = ensureDisplayBoxForContainer(layoutBox.parent(), displayBoxTree, ancestorStack, boxes);
hasInlineBox = hasInlineBox || parentDisplayBoxNodeIndex || lineRun.isInlineBoxStart() || lineRun.isLineSpanningInlineBoxStart();
if (lineRun.isText()) {
auto logicalRect = lineBox.logicalRectForTextRun(lineRun);
auto visualRect = visualRectRelativeToRoot(logicalRect);
auto wordSpacingMargin = lineRun.isWordSeparator() ? layoutBox.style().fontCascade().wordSpacing() : 0.0f;
isHorizontalWritingMode ? visualRect.moveHorizontally(wordSpacingMargin) : visualRect.moveVertically(wordSpacingMargin);
appendTextDisplayBox(lineRun, visualRect, boxes);
contentRightInInlineDirectionVisualOrder += logicalRect.width() + wordSpacingMargin;
displayBoxTree.append(parentDisplayBoxNodeIndex, boxes.size() - 1);
continue;
}
if (lineRun.isSoftLineBreak()) {
auto visualRect = visualRectRelativeToRoot(lineBox.logicalRectForTextRun(lineRun));
ASSERT((isHorizontalWritingMode && !visualRect.width()) || (!isHorizontalWritingMode && !visualRect.height()));
appendSoftLineBreakDisplayBox(lineRun, visualRect, boxes);
displayBoxTree.append(parentDisplayBoxNodeIndex, boxes.size() - 1);
continue;
}
if (lineRun.isHardLineBreak()) {
auto visualRect = visualRectRelativeToRoot(lineBox.logicalRectForLineBreakBox(layoutBox));
ASSERT((isHorizontalWritingMode && !visualRect.width()) || (!isHorizontalWritingMode && !visualRect.height()));
appendHardLineBreakDisplayBox(lineRun, visualRect, boxes);
displayBoxTree.append(parentDisplayBoxNodeIndex, boxes.size() - 1);
continue;
}
if (lineRun.isBox() || lineRun.isListMarker()) {
auto& boxGeometry = formattingState().boxGeometry(layoutBox);
auto logicalRect = lineBox.logicalBorderBoxForAtomicInlineLevelBox(layoutBox, boxGeometry);
auto visualRect = visualRectRelativeToRoot(logicalRect);
auto isLeftToRightDirection = layoutBox.parent().style().isLeftToRightDirection();
auto boxMarginLeft = marginLeftInInlineDirection(boxGeometry, isLeftToRightDirection);
isHorizontalWritingMode ? visualRect.moveHorizontally(boxMarginLeft) : visualRect.moveVertically(boxMarginLeft);
appendAtomicInlineLevelDisplayBox(lineRun, visualRect, boxes);
contentRightInInlineDirectionVisualOrder += boxMarginLeft + logicalRect.width() + marginRightInInlineDirection(boxGeometry, isLeftToRightDirection);
displayBoxTree.append(parentDisplayBoxNodeIndex, boxes.size() - 1);
continue;
}
if (lineRun.isInlineBoxStart() || lineRun.isLineSpanningInlineBoxStart()) {
// FIXME: While we should only get here with empty inline boxes, there are
// some cases where the inline box has some content on the paragraph level (at bidi split) but line breaking renders it empty
// or their content is completely collapsed.
// Such inline boxes should also be handled here.
if (!lineBox.hasContent()) {
// FIXME: It's expected to not have any inline boxes on empty lines. They make the line taller. We should reconsider this.
setInlineBoxGeometry(layoutBox, { { }, { } }, true);
} else if (!lineBox.inlineLevelBoxForLayoutBox(layoutBox).hasContent()) {
appendInlineDisplayBoxAtBidiBoundary(layoutBox, boxes);
createdDisplayBoxNodeForContainerBoxAndPushToAncestorStack(downcast<ContainerBox>(layoutBox), boxes.size() - 1, parentDisplayBoxNodeIndex, displayBoxTree, ancestorStack);
}
continue;
}
ASSERT_NOT_REACHED();
}
};
createDisplayBoxesInVisualOrder();
auto handleInlineBoxes = [&] {
if (!hasInlineBox)
return;
IsFirstLastIndexesMap isFirstLastIndexesMap;
auto computeIsFirstIsLastBox = [&] {
ASSERT(boxes[0].isRootInlineBox());
for (size_t index = 1; index < boxes.size(); ++index) {
if (!boxes[index].isNonRootInlineBox())
continue;
auto& layoutBox = boxes[index].layoutBox();
auto isFirstBox = lineBox.inlineLevelBoxForLayoutBox(layoutBox).isFirstBox();
auto isLastBox = lineBox.inlineLevelBoxForLayoutBox(layoutBox).isLastBox();
if (!isFirstBox && !isLastBox)
continue;
if (isFirstBox) {
auto isFirstLastIndexes = isFirstLastIndexesMap.get(&layoutBox);
if (!isFirstLastIndexes.first || isLastBox)
isFirstLastIndexesMap.set(&layoutBox, IsFirstLastIndex { isFirstLastIndexes.first.value_or(index), isLastBox ? index : isFirstLastIndexes.last });
continue;
}
if (isLastBox) {
ASSERT(!isFirstBox);
isFirstLastIndexesMap.set(&layoutBox, IsFirstLastIndex { { }, index });
continue;
}
}
};
computeIsFirstIsLastBox();
auto adjustVisualGeometryWithInlineBoxes = [&] {
auto contentRightInInlineDirectionVisualOrder = contentStartInInlineDirectionVisualOrder;
for (auto childDisplayBoxNodeIndex : displayBoxTree.root().children)
adjustVisualGeometryForDisplayBox(childDisplayBoxNodeIndex, contentRightInInlineDirectionVisualOrder, lineLogicalTop, displayBoxTree, boxes, lineBox, isFirstLastIndexesMap);
};
adjustVisualGeometryWithInlineBoxes();
};
handleInlineBoxes();
auto handleTrailingOpenInlineBoxes = [&] {
for (auto& lineRun : makeReversedRange(lineContent.runs)) {
if (!lineRun.isInlineBoxStart() || lineRun.bidiLevel() != InlineItem::opaqueBidiLevel)
break;
// These are trailing inline box start runs (without the closing inline box end <span> <-line breaks here</span>).
// They don't participate in visual reordering (see createDisplayBoxesInVisualOrder above) as they we don't find them
// empty at inline building time (see setBidiLevelForOpaqueInlineItems) due to trailing whitespace.
// Non-empty inline boxes are normally get their display boxes generated when we process their content runs, but
// these trailing runs have their content on the subsequent line(s).
appendInlineBoxDisplayBox(lineRun
, lineBox.inlineLevelBoxForLayoutBox(lineRun.layoutBox())
, boxes.last().visualRectIgnoringBlockDirection()
, lineBox.hasContent()
, boxes);
}
};
handleTrailingOpenInlineBoxes();
}
void InlineDisplayContentBuilder::processOverflownRunsForEllipsis(DisplayBoxes& boxes, InlineLayoutUnit lineBoxRight)
{
if (root().style().textOverflow() != TextOverflow::Ellipsis)
return;
auto& rootInlineBox = boxes[0];
ASSERT(rootInlineBox.isRootInlineBox());
if (rootInlineBox.right() <= lineBoxRight) {
ASSERT(boxes.last().right() <= lineBoxRight);
return;
}
static MainThreadNeverDestroyed<const AtomString> ellipsisStr(&horizontalEllipsis, 1);
auto ellipsisRun = WebCore::TextRun { ellipsisStr->string() };
auto ellipsisWidth = !m_lineIndex ? root().firstLineStyle().fontCascade().width(ellipsisRun) : root().style().fontCascade().width(ellipsisRun);
auto firstTruncatedBoxIndex = boxes.size();
for (auto index = boxes.size(); index--;) {
auto& displayBox = boxes[index];
if (displayBox.left() >= lineBoxRight) {
// Fully overflown boxes are collapsed.
displayBox.truncate();
continue;
}
// We keep truncating content until after we can accommodate the ellipsis content
// 1. fully truncate in case of inline level boxes (ie non-text content) or if ellipsis content is wider than the overflowing one.
// 2. partially truncated to make room for the ellipsis box.
auto availableRoomForEllipsis = lineBoxRight - displayBox.left();
if (availableRoomForEllipsis <= ellipsisWidth) {
// Can't accommodate the ellipsis content here. We need to truncate non-overflowing boxes too.
displayBox.truncate();
continue;
}
auto truncatedWidth = InlineLayoutUnit { };
if (displayBox.isText()) {
auto text = *displayBox.text();
// FIXME: Check if it needs adjustment for RTL direction.
truncatedWidth = TextUtil::breakWord(downcast<InlineTextBox>(displayBox.layoutBox()), text.start(), text.length(), displayBox.width(), availableRoomForEllipsis - ellipsisWidth, { }, displayBox.style().fontCascade()).logicalWidth;
}
displayBox.truncate(truncatedWidth);
firstTruncatedBoxIndex = index;
break;
}
ASSERT(firstTruncatedBoxIndex < boxes.size());
// Collapse truncated runs.
auto contentRight = boxes[firstTruncatedBoxIndex].right();
for (auto index = firstTruncatedBoxIndex + 1; index < boxes.size(); ++index)
boxes[index].moveHorizontally(contentRight - boxes[index].left());
// And append the ellipsis box as the trailing item.
auto ellispisBoxRect = InlineRect { rootInlineBox.top(), contentRight, ellipsisWidth, rootInlineBox.height() };
boxes.append({ m_lineIndex
, InlineDisplay::Box::Type::Ellipsis
, root()
, UBIDI_DEFAULT_LTR
, ellispisBoxRect
, ellispisBoxRect
, { }
, InlineDisplay::Box::Text { 0, 1, ellipsisStr->string() } });
}
void InlineDisplayContentBuilder::collectInkOverflowForInlineBoxes(DisplayBoxes& boxes)
{
if (!m_contentHasInkOverflow)
return;
// Visit the inline boxes and propagate ink overflow to their parents -except to the root inline box.
// (e.g. <span style="font-size: 10px;">Small font size<span style="font-size: 300px;">Larger font size. This overflows the top most span.</span></span>).
auto accumulatedInkOverflowRect = InlineRect { { }, { } };
for (size_t index = boxes.size(); index--;) {
auto& displayBox = boxes[index];
auto mayHaveInkOverflow = displayBox.isText() || displayBox.isAtomicInlineLevelBox() || displayBox.isGenericInlineLevelBox() || displayBox.isNonRootInlineBox();
if (!mayHaveInkOverflow)
continue;
if (displayBox.isNonRootInlineBox() && !accumulatedInkOverflowRect.isEmpty())
displayBox.adjustInkOverflow(accumulatedInkOverflowRect);
// We stop collecting ink overflow for at root inline box (i.e. don't inflate the root inline box with the inline content here).
auto parentBoxIsRoot = &displayBox.layoutBox().parent() == &root();
if (parentBoxIsRoot)
accumulatedInkOverflowRect = InlineRect { { }, { } };
else if (accumulatedInkOverflowRect.isEmpty())
accumulatedInkOverflowRect = displayBox.inkOverflow();
else
accumulatedInkOverflowRect.expandToContain(displayBox.inkOverflow());
}
}
static float logicalBottomForTextDecorationContent(const DisplayBoxes& boxes, bool isHorizontalWritingMode)
{
auto logicalBottom = std::optional<float> { };
for (auto& displayBox : boxes) {
if (displayBox.isRootInlineBox())
continue;
if (!displayBox.style().textDecorationsInEffect().contains(TextDecorationLine::Underline))
continue;
if (displayBox.isText() || displayBox.style().textDecorationSkipInk() == TextDecorationSkipInk::None) {
auto contentLogicalBottom = isHorizontalWritingMode ? displayBox.bottom() : displayBox.right();
logicalBottom = logicalBottom ? std::max(*logicalBottom, contentLogicalBottom) : contentLogicalBottom;
}
}
// This function is not called unless there's at least one run on the line with TextDecorationLine::Underline.
ASSERT(logicalBottom);
return logicalBottom.value_or(0.f);
}
void InlineDisplayContentBuilder::collectInkOverflowForTextDecorations(DisplayBoxes& boxes, const InlineDisplay::Line& displayLine)
{
auto logicalBottomForTextDecoration = std::optional<float> { };
auto writingMode = root().style().writingMode();
auto isHorizontalWritingMode = WebCore::isHorizontalWritingMode(writingMode);
for (auto& displayBox : boxes) {
if (!displayBox.isText())
continue;
auto& style = displayBox.style();
auto textDecorations = style.textDecorationsInEffect();
if (!textDecorations)
continue;
auto underlineOffset = [&]() -> std::optional<float> {
if (!textDecorations.contains(TextDecorationLine::Underline))
return { };
if (!logicalBottomForTextDecoration)
logicalBottomForTextDecoration = logicalBottomForTextDecorationContent(boxes, isHorizontalWritingMode);
auto textRunLogicalOffsetFromLineBottom = *logicalBottomForTextDecoration - (isHorizontalWritingMode ? displayBox.bottom() : displayBox.right());
// Compensate for the integral ceiling in GraphicsContext::computeLineBoundsAndAntialiasingModeForText()
return computeUnderlineOffset({ style, defaultGap(style), UnderlineOffsetArguments::TextUnderlinePositionUnder { displayLine.baselineType(), displayBox.height(), textRunLogicalOffsetFromLineBottom } }) + 1.0f;
};
auto decorationOverflow = visualOverflowForDecorations(style, underlineOffset());
if (!decorationOverflow.isEmpty()) {
m_contentHasInkOverflow = true;
auto inflatedVisualOverflowRect = [&] {
auto inkOverflowRect = displayBox.inkOverflow();
switch (writingMode) {
case WritingMode::TopToBottom:
inkOverflowRect.inflate(decorationOverflow.left, decorationOverflow.top, decorationOverflow.right, decorationOverflow.bottom);
break;
case WritingMode::LeftToRight:
inkOverflowRect.inflate(decorationOverflow.bottom, decorationOverflow.right, decorationOverflow.top, decorationOverflow.left);
break;
case WritingMode::RightToLeft:
inkOverflowRect.inflate(decorationOverflow.top, decorationOverflow.right, decorationOverflow.bottom, decorationOverflow.left);
break;
default:
ASSERT_NOT_REACHED();
break;
}
return inkOverflowRect;
};
displayBox.adjustInkOverflow(inflatedVisualOverflowRect());
}
}
}
void InlineDisplayContentBuilder::computeIsFirstIsLastBoxForInlineContent(DisplayBoxes& boxes)
{
HashMap<const Box*, size_t> lastDisplayBoxForLayoutBoxIndexes;
ASSERT(boxes[0].isRootInlineBox());
boxes[0].setIsFirstForLayoutBox(true);
size_t lastRootInlineBoxIndex = 0;
for (size_t index = 1; index < boxes.size(); ++index) {
auto& displayBox = boxes[index];
if (displayBox.isRootInlineBox()) {
lastRootInlineBoxIndex = index;
continue;
}
auto& layoutBox = displayBox.layoutBox();
if (lastDisplayBoxForLayoutBoxIndexes.set(&layoutBox, index).isNewEntry)
displayBox.setIsFirstForLayoutBox(true);
}
for (auto index : lastDisplayBoxForLayoutBoxIndexes.values())
boxes[index].setIsLastForLayoutBox(true);
boxes[lastRootInlineBoxIndex].setIsLastForLayoutBox(true);
}
InlineRect InlineDisplayContentBuilder::flipLogicalRectToVisualForWritingModeWithinLine(const InlineRect& logicalRect, const InlineRect& lineLogicalRect, WritingMode writingMode) const
{
switch (writingMode) {
case WritingMode::TopToBottom:
return logicalRect;
case WritingMode::LeftToRight: {
// Flip content such that the top (visual left) is now relative to the line bottom instead of the line top.
auto bottomOffset = lineLogicalRect.height() - logicalRect.bottom();
return { logicalRect.left(), bottomOffset, logicalRect.height(), logicalRect.width() };
}
case WritingMode::RightToLeft:
// See InlineFormattingGeometry for more info.
return { logicalRect.left(), logicalRect.top(), logicalRect.height(), logicalRect.width() };
default:
ASSERT_NOT_REACHED();
break;
}
return logicalRect;
}
InlineRect InlineDisplayContentBuilder::flipLogicalRectToVisualForWritingMode(const InlineRect& logicalRect, WritingMode writingMode) const
{
switch (writingMode) {
case WritingMode::TopToBottom:
return logicalRect;
case WritingMode::LeftToRight:
case WritingMode::RightToLeft:
// See InlineFormattingGeometry for more info.
return { logicalRect.left(), logicalRect.top(), logicalRect.height(), logicalRect.width() };
default:
ASSERT_NOT_REACHED();
break;
}
return logicalRect;
}
InlineRect InlineDisplayContentBuilder::flipRootInlineBoxRectToVisualForWritingMode(const InlineRect& rootInlineBoxLogicalRect, const InlineDisplay::Line& displayLine, WritingMode writingMode) const
{
switch (writingMode) {
case WritingMode::TopToBottom: {
auto visualRect = rootInlineBoxLogicalRect;
visualRect.moveBy({ displayLine.left() + displayLine.contentLogicalOffset(), displayLine.top() });
return visualRect;
}
case WritingMode::LeftToRight:
case WritingMode::RightToLeft: {
// See InlineFormattingGeometry for more info.
auto visualRect = InlineRect { rootInlineBoxLogicalRect.left(), rootInlineBoxLogicalRect.top(), rootInlineBoxLogicalRect.height(), rootInlineBoxLogicalRect.width() };
visualRect.moveBy({ displayLine.left(), displayLine.top() + displayLine.contentLogicalOffset() });
return visualRect;
}
default:
ASSERT_NOT_REACHED();
break;
}
return rootInlineBoxLogicalRect;
}
void InlineDisplayContentBuilder::setLeftForWritingMode(InlineDisplay::Box& displayBox, InlineLayoutUnit logicalLeft, WritingMode writingMode) const
{
switch (writingMode) {
case WritingMode::TopToBottom:
displayBox.setLeft(logicalLeft);
break;
case WritingMode::LeftToRight:
case WritingMode::RightToLeft:
displayBox.setTop(logicalLeft);
break;
default:
ASSERT_NOT_REACHED();
break;
}
}
void InlineDisplayContentBuilder::setRightForWritingMode(InlineDisplay::Box& displayBox, InlineLayoutUnit logicalRight, WritingMode writingMode) const
{
switch (writingMode) {
case WritingMode::TopToBottom:
displayBox.setRight(logicalRight);
break;
case WritingMode::LeftToRight:
case WritingMode::RightToLeft:
displayBox.setBottom(logicalRight);
break;
default:
ASSERT_NOT_REACHED();
break;
}
}
InlineLayoutPoint InlineDisplayContentBuilder::movePointHorizontallyForWritingMode(const InlineLayoutPoint& logicalPoint, InlineLayoutUnit horizontalOffset, WritingMode writingMode) const
{
auto visualPoint = logicalPoint;
switch (writingMode) {
case WritingMode::TopToBottom:
visualPoint.moveBy(FloatPoint { horizontalOffset, { } });
break;
case WritingMode::LeftToRight:
case WritingMode::RightToLeft: {
visualPoint.moveBy(FloatPoint { { }, horizontalOffset });
break;
}
default:
ASSERT_NOT_REACHED();
break;
}
return visualPoint;
}
}
}
#endif