blob: cd231fcd38f836e63666aaab7a31744e949e50b7 [file] [log] [blame]
/*
* Copyright (C) 2018 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 "TextUtil.h"
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
#include "BreakLines.h"
#include "FontCascade.h"
#include "InlineTextItem.h"
#include "Latin1TextIterator.h"
#include "LayoutInlineTextBox.h"
#include "RenderBox.h"
#include "RenderStyle.h"
#include "SurrogatePairAwareTextIterator.h"
#include <unicode/ubidi.h>
#include <wtf/text/TextBreakIterator.h>
namespace WebCore {
namespace Layout {
InlineLayoutUnit TextUtil::width(const InlineTextItem& inlineTextItem, const FontCascade& fontCascade, InlineLayoutUnit contentLogicalLeft)
{
return TextUtil::width(inlineTextItem, fontCascade, inlineTextItem.start(), inlineTextItem.end(), contentLogicalLeft);
}
InlineLayoutUnit TextUtil::width(const InlineTextItem& inlineTextItem, const FontCascade& fontCascade, unsigned from, unsigned to, InlineLayoutUnit contentLogicalLeft)
{
RELEASE_ASSERT(from >= inlineTextItem.start());
RELEASE_ASSERT(to <= inlineTextItem.end());
if (inlineTextItem.isWhitespace() && !TextUtil::shouldPreserveSpacesAndTabs(inlineTextItem.layoutBox())) {
auto spaceWidth = fontCascade.spaceWidth();
return std::isnan(spaceWidth) ? 0.0f : std::isinf(spaceWidth) ? maxInlineLayoutUnit() : spaceWidth;
}
return TextUtil::width(inlineTextItem.inlineTextBox(), fontCascade, from, to, contentLogicalLeft);
}
InlineLayoutUnit TextUtil::width(const InlineTextBox& inlineTextBox, const FontCascade& fontCascade, unsigned from, unsigned to, InlineLayoutUnit contentLogicalLeft, UseTrailingWhitespaceMeasuringOptimization useTrailingWhitespaceMeasuringOptimization)
{
if (from == to)
return 0;
auto text = inlineTextBox.content();
ASSERT(to <= text.length());
auto hasKerningOrLigatures = fontCascade.enableKerning() || fontCascade.requiresShaping();
// The "non-whitespace" + "whitespace" pattern is very common for inline content and since most of the "non-whitespace" runs end up with
// their "whitespace" pair on the line (notable exception is when trailing whitespace is trimmed).
// Including the trailing whitespace here enables us to cut the number of text measures when placing content on the line.
auto extendedMeasuring = useTrailingWhitespaceMeasuringOptimization == UseTrailingWhitespaceMeasuringOptimization::Yes && hasKerningOrLigatures && to < text.length() && text[to] == space;
if (extendedMeasuring)
++to;
float width = 0;
if (inlineTextBox.canUseSimplifiedContentMeasuring())
width = fontCascade.widthForSimpleText(StringView(text).substring(from, to - from));
else {
WebCore::TextRun run(StringView(text).substring(from, to - from), contentLogicalLeft);
auto& style = inlineTextBox.style();
if (!style.collapseWhiteSpace() && style.tabSize())
run.setTabSize(true, style.tabSize());
width = fontCascade.width(run);
}
if (extendedMeasuring)
width -= (fontCascade.spaceWidth() + fontCascade.wordSpacing());
return std::isnan(width) ? 0.0f : std::isinf(width) ? maxInlineLayoutUnit() : width;
}
InlineLayoutUnit TextUtil::trailingWhitespaceWidth(const InlineTextBox& inlineTextBox, const FontCascade& fontCascade, size_t startPosition, size_t endPosition)
{
auto text = inlineTextBox.content();
ASSERT(endPosition > startPosition + 1);
ASSERT(text[endPosition - 1] == space);
return width(inlineTextBox, fontCascade, startPosition, endPosition, { }, TextUtil::UseTrailingWhitespaceMeasuringOptimization::Yes) -
width(inlineTextBox, fontCascade, startPosition, endPosition - 1, { }, TextUtil::UseTrailingWhitespaceMeasuringOptimization::No);
}
template <typename TextIterator>
static void fallbackFontsForRunWithIterator(HashSet<const Font*>& fallbackFonts, const FontCascade& fontCascade, const TextRun& run, TextIterator& textIterator)
{
auto isRTL = run.rtl();
auto isSmallCaps = fontCascade.isSmallCaps();
auto& primaryFont = fontCascade.primaryFont();
UChar32 currentCharacter = 0;
unsigned clusterLength = 0;
while (textIterator.consume(currentCharacter, clusterLength)) {
auto addFallbackFontForCharacterIfApplicable = [&](auto character) {
if (isSmallCaps && character != u_toupper(character))
character = u_toupper(character);
auto glyphData = fontCascade.glyphDataForCharacter(character, isRTL);
if (glyphData.glyph && glyphData.font && glyphData.font != &primaryFont) {
auto isNonSpacingMark = U_MASK(u_charType(character)) & U_GC_MN_MASK;
if (isNonSpacingMark || glyphData.font->widthForGlyph(glyphData.glyph))
fallbackFonts.add(glyphData.font);
}
};
addFallbackFontForCharacterIfApplicable(currentCharacter);
textIterator.advance(clusterLength);
}
}
TextUtil::FallbackFontList TextUtil::fallbackFontsForRun(const Line::Run& run, const RenderStyle& style)
{
ASSERT(run.isText());
auto& inlineTextBox = downcast<InlineTextBox>(run.layoutBox());
if (inlineTextBox.canUseSimplifiedContentMeasuring()) {
// Simplified text measuring works with primary font only.
return { };
}
TextUtil::FallbackFontList fallbackFonts;
auto collectFallbackFonts = [&](const auto& textRun) {
if (textRun.text().isEmpty())
return;
if (textRun.is8Bit()) {
auto textIterator = Latin1TextIterator { textRun.data8(0), 0, textRun.length(), textRun.length() };
fallbackFontsForRunWithIterator(fallbackFonts, style.fontCascade(), textRun, textIterator);
return;
}
auto textIterator = SurrogatePairAwareTextIterator { textRun.data16(0), 0, textRun.length(), textRun.length() };
fallbackFontsForRunWithIterator(fallbackFonts, style.fontCascade(), textRun, textIterator);
};
auto text = *run.textContent();
if (text.needsHyphen)
collectFallbackFonts(TextRun { StringView(style.hyphenString().string()), { }, { }, DefaultExpansion, style.direction() });
collectFallbackFonts(TextRun { StringView(inlineTextBox.content()).substring(text.start, text.length), { }, { }, DefaultExpansion, style.direction() });
return fallbackFonts;
}
TextUtil::WordBreakLeft TextUtil::breakWord(const InlineTextItem& inlineTextItem, const FontCascade& fontCascade, InlineLayoutUnit textWidth, InlineLayoutUnit availableWidth, InlineLayoutUnit contentLogicalLeft)
{
return breakWord(inlineTextItem.inlineTextBox(), inlineTextItem.start(), inlineTextItem.length(), textWidth, availableWidth, contentLogicalLeft, fontCascade);
}
TextUtil::WordBreakLeft TextUtil::breakWord(const InlineTextBox& inlineTextBox, size_t startPosition, size_t length, InlineLayoutUnit textWidth, InlineLayoutUnit availableWidth, InlineLayoutUnit contentLogicalLeft, const FontCascade& fontCascade)
{
ASSERT(availableWidth >= 0);
ASSERT(length);
auto text = inlineTextBox.content();
if (inlineTextBox.canUseSimpleFontCodePath()) {
auto findBreakingPositionInSimpleText = [&] {
auto userPerceivedCharacterBoundaryAlignedIndex = [&] (auto index) -> size_t {
if (text.is8Bit())
return index;
auto alignedStartIndex = index;
U16_SET_CP_START(text, startPosition, alignedStartIndex);
ASSERT(alignedStartIndex >= startPosition);
return alignedStartIndex;
};
auto nextUserPerceivedCharacterIndex = [&] (auto index) -> size_t {
if (text.is8Bit())
return index + 1;
U16_FWD_1(text, index, length);
return index;
};
auto left = startPosition;
auto right = left + length - 1;
// Pathological case of (extremely)long string and narrow lines.
// Adjust the range so that we can pick a reasonable midpoint.
auto averageCharacterWidth = InlineLayoutUnit { textWidth / length };
size_t startOffset = 2 * availableWidth / averageCharacterWidth;
right = userPerceivedCharacterBoundaryAlignedIndex(std::min(left + startOffset, right));
// Preserve the left width for the final split position so that we don't need to remeasure the left side again.
auto leftSideWidth = InlineLayoutUnit { 0 };
while (left < right) {
auto middle = userPerceivedCharacterBoundaryAlignedIndex((left + right) / 2);
ASSERT(middle >= left && middle < right);
auto endOfMiddleCharacter = nextUserPerceivedCharacterIndex(middle);
auto width = TextUtil::width(inlineTextBox, fontCascade, startPosition, endOfMiddleCharacter, contentLogicalLeft);
if (width < availableWidth) {
left = endOfMiddleCharacter;
leftSideWidth = width;
} else if (width > availableWidth)
right = middle;
else {
right = endOfMiddleCharacter;
leftSideWidth = width;
break;
}
}
RELEASE_ASSERT(right >= startPosition);
return TextUtil::WordBreakLeft { right - startPosition, leftSideWidth };
};
return findBreakingPositionInSimpleText();
}
auto graphemeClusterIterator = NonSharedCharacterBreakIterator { StringView { text }.substring(startPosition, length) };
auto leftSide = TextUtil::WordBreakLeft { };
for (auto clusterStartPosition = ubrk_next(graphemeClusterIterator); clusterStartPosition != UBRK_DONE; clusterStartPosition = ubrk_next(graphemeClusterIterator)) {
auto width = TextUtil::width(inlineTextBox, fontCascade, startPosition, startPosition + clusterStartPosition, contentLogicalLeft);
if (width > availableWidth)
return leftSide;
leftSide = { static_cast<size_t>(clusterStartPosition), width };
}
// This content is not supposed to fit availableWidth.
ASSERT_NOT_REACHED();
return { };
}
unsigned TextUtil::findNextBreakablePosition(LazyLineBreakIterator& lineBreakIterator, unsigned startPosition, const RenderStyle& style)
{
auto keepAllWordsForCJK = style.wordBreak() == WordBreak::KeepAll;
auto breakNBSP = style.autoWrap() && style.nbspMode() == NBSPMode::Space;
if (keepAllWordsForCJK) {
if (breakNBSP)
return nextBreakablePositionKeepingAllWords(lineBreakIterator, startPosition);
return nextBreakablePositionKeepingAllWordsIgnoringNBSP(lineBreakIterator, startPosition);
}
if (lineBreakIterator.mode() == LineBreakIteratorMode::Default) {
if (breakNBSP)
return WebCore::nextBreakablePosition(lineBreakIterator, startPosition);
return nextBreakablePositionIgnoringNBSP(lineBreakIterator, startPosition);
}
if (breakNBSP)
return nextBreakablePositionWithoutShortcut(lineBreakIterator, startPosition);
return nextBreakablePositionIgnoringNBSPWithoutShortcut(lineBreakIterator, startPosition);
}
bool TextUtil::shouldPreserveSpacesAndTabs(const Box& layoutBox)
{
// https://www.w3.org/TR/css-text-3/#white-space-property
auto whitespace = layoutBox.style().whiteSpace();
return whitespace == WhiteSpace::Pre || whitespace == WhiteSpace::PreWrap || whitespace == WhiteSpace::BreakSpaces;
}
bool TextUtil::shouldPreserveNewline(const Box& layoutBox)
{
auto whitespace = layoutBox.style().whiteSpace();
// https://www.w3.org/TR/css-text-3/#white-space-property
return whitespace == WhiteSpace::Pre || whitespace == WhiteSpace::PreWrap || whitespace == WhiteSpace::BreakSpaces || whitespace == WhiteSpace::PreLine;
}
bool TextUtil::isWrappingAllowed(const RenderStyle& style)
{
// Do not try to wrap overflown 'pre' and 'no-wrap' content to next line.
return style.whiteSpace() != WhiteSpace::Pre && style.whiteSpace() != WhiteSpace::NoWrap;
}
LineBreakIteratorMode TextUtil::lineBreakIteratorMode(LineBreak lineBreak)
{
switch (lineBreak) {
case LineBreak::Auto:
case LineBreak::AfterWhiteSpace:
case LineBreak::Anywhere:
return LineBreakIteratorMode::Default;
case LineBreak::Loose:
return LineBreakIteratorMode::Loose;
case LineBreak::Normal:
return LineBreakIteratorMode::Normal;
case LineBreak::Strict:
return LineBreakIteratorMode::Strict;
}
ASSERT_NOT_REACHED();
return LineBreakIteratorMode::Default;
}
bool TextUtil::containsStrongDirectionalityText(StringView text)
{
if (text.is8Bit())
return false;
auto length = text.length();
for (size_t position = 0; position < length;) {
UChar32 character;
U16_NEXT(text.characters16(), position, length, character);
auto bidiCategory = u_charDirection(character);
bool hasBidiContent = bidiCategory == U_RIGHT_TO_LEFT
|| bidiCategory == U_RIGHT_TO_LEFT_ARABIC
|| bidiCategory == U_RIGHT_TO_LEFT_EMBEDDING
|| bidiCategory == U_RIGHT_TO_LEFT_OVERRIDE
|| bidiCategory == U_LEFT_TO_RIGHT_EMBEDDING
|| bidiCategory == U_LEFT_TO_RIGHT_OVERRIDE
|| bidiCategory == U_POP_DIRECTIONAL_FORMAT;
if (hasBidiContent)
return true;
}
return false;
}
size_t TextUtil::firstUserPerceivedCharacterLength(const InlineTextItem& inlineTextItem)
{
auto& inlineTextBox = inlineTextItem.inlineTextBox();
auto textContent = inlineTextBox.content();
RELEASE_ASSERT(!textContent.isEmpty());
if (textContent.is8Bit())
return 1;
if (inlineTextBox.canUseSimpleFontCodePath()) {
UChar32 character;
size_t endOfCodePoint = inlineTextItem.start();
U16_NEXT(textContent.characters16(), endOfCodePoint, textContent.length(), character);
ASSERT(endOfCodePoint > inlineTextItem.start());
return endOfCodePoint - inlineTextItem.start();
}
auto graphemeClustersIterator = NonSharedCharacterBreakIterator { textContent };
auto nextPosition = ubrk_following(graphemeClustersIterator, inlineTextItem.start());
if (nextPosition == UBRK_DONE)
return inlineTextItem.length();
return nextPosition - inlineTextItem.start();
}
TextDirection TextUtil::directionForTextContent(StringView content)
{
if (content.is8Bit())
return TextDirection::LTR;
return ubidi_getBaseDirection(content.characters16(), content.length()) == UBIDI_RTL ? TextDirection::RTL : TextDirection::LTR;
}
}
}
#endif