| /* |
| * 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 |