| /* |
| * 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 "InlineContentBreaker.h" |
| |
| #if ENABLE(LAYOUT_FORMATTING_CONTEXT) |
| |
| #include "FontCascade.h" |
| #include "Hyphenation.h" |
| #include "InlineItem.h" |
| #include "InlineTextItem.h" |
| #include "LayoutContainerBox.h" |
| #include "TextUtil.h" |
| |
| namespace WebCore { |
| namespace Layout { |
| |
| |
| #if ASSERT_ENABLED |
| static inline bool hasTrailingTextContent(const InlineContentBreaker::ContinuousContent& continuousContent) |
| { |
| for (auto& run : makeReversedRange(continuousContent.runs())) { |
| auto& inlineItem = run.inlineItem; |
| if (inlineItem.isInlineBoxStart() || inlineItem.isInlineBoxEnd()) |
| continue; |
| return inlineItem.isText(); |
| } |
| return false; |
| } |
| #endif |
| |
| static inline bool hasLeadingTextContent(const InlineContentBreaker::ContinuousContent& continuousContent) |
| { |
| for (auto& run : continuousContent.runs()) { |
| auto& inlineItem = run.inlineItem; |
| if (inlineItem.isInlineBoxStart() || inlineItem.isInlineBoxEnd()) |
| continue; |
| return inlineItem.isText(); |
| } |
| return false; |
| } |
| |
| static inline bool hasTextRun(const InlineContentBreaker::ContinuousContent& continuousContent) |
| { |
| // <span>text</span> is considered a text run even with the [inline box start][inline box end] inline items. |
| // Based on standards commit boundary rules it would be enough to check the first inline item, but due to the table quirk, we can have |
| // image and text next to each other inside a continuous set of runs (see InlineFormattingContext::Quirks::hasSoftWrapOpportunityAtImage). |
| for (auto& run : continuousContent.runs()) { |
| if (run.inlineItem.isText()) |
| return true; |
| } |
| return false; |
| } |
| |
| static inline std::optional<size_t> nextTextRunIndex(const InlineContentBreaker::ContinuousContent::RunList& runs, size_t startIndex) |
| { |
| for (auto index = startIndex + 1; index < runs.size(); ++index) { |
| if (runs[index].inlineItem.isText()) |
| return index; |
| } |
| return { }; |
| } |
| |
| static inline bool isVisuallyEmptyWhitespaceContent(const InlineContentBreaker::ContinuousContent& continuousContent) |
| { |
| // [<span></span> ] [<span> </span>] [ <span style="padding: 0px;"></span>] are all considered visually empty whitespace content. |
| // [<span style="border: 1px solid red"></span> ] while this is whitespace content only, it is not considered visually empty. |
| ASSERT(!continuousContent.runs().isEmpty()); |
| auto hasWhitespace = false; |
| for (auto& run : continuousContent.runs()) { |
| auto& inlineItem = run.inlineItem; |
| // FIXME: check if visual decoration makes a difference here e.g. padding border. |
| if (inlineItem.isInlineBoxStart() || inlineItem.isInlineBoxEnd()) |
| continue; |
| auto isWhitespace = inlineItem.isText() && downcast<InlineTextItem>(inlineItem).isWhitespace(); |
| if (!isWhitespace) |
| return false; |
| hasWhitespace = true; |
| } |
| return hasWhitespace; |
| } |
| |
| static inline bool isNonContentRunsOnly(const InlineContentBreaker::ContinuousContent& continuousContent) |
| { |
| // <span></span> <- non content runs. |
| for (auto& run : continuousContent.runs()) { |
| auto& inlineItem = run.inlineItem; |
| if (inlineItem.isInlineBoxStart() || inlineItem.isInlineBoxEnd()) |
| continue; |
| return false; |
| } |
| return true; |
| } |
| |
| static inline std::optional<size_t> firstTextRunIndex(const InlineContentBreaker::ContinuousContent& continuousContent) |
| { |
| auto& runs = continuousContent.runs(); |
| for (size_t index = 0; index < runs.size(); ++index) { |
| if (runs[index].inlineItem.isText()) |
| return index; |
| } |
| return { }; |
| } |
| |
| InlineContentBreaker::InlineContentBreaker(std::optional<IntrinsicWidthMode> intrinsicWidthMode) |
| : m_intrinsicWidthMode(intrinsicWidthMode) |
| { |
| } |
| |
| bool InlineContentBreaker::shouldKeepEndOfLineWhitespace(const ContinuousContent& continuousContent) const |
| { |
| // Grab the style and check for white-space property to decide whether we should let this whitespace content overflow the current line. |
| // Note that the "keep" in this context means we let the whitespace content sit on the current line. |
| // It might very well get collapsed when we close the line (normal/nowrap/pre-line). |
| // See https://www.w3.org/TR/css-text-3/#white-space-property |
| auto whitespace = continuousContent.runs()[*firstTextRunIndex(continuousContent)].style.whiteSpace(); |
| return whitespace == WhiteSpace::Normal || whitespace == WhiteSpace::NoWrap || whitespace == WhiteSpace::PreWrap || whitespace == WhiteSpace::PreLine; |
| } |
| |
| InlineContentBreaker::Result InlineContentBreaker::processInlineContent(const ContinuousContent& candidateContent, const LineStatus& lineStatus) |
| { |
| ASSERT(!std::isnan(lineStatus.availableWidth)); |
| auto processCandidateContent = [&] { |
| if (candidateContent.logicalWidth() <= lineStatus.availableWidth) |
| return Result { Result::Action::Keep }; |
| return processOverflowingContent(candidateContent, lineStatus); |
| }; |
| |
| auto result = processCandidateContent(); |
| if (result.action == Result::Action::Wrap && lineStatus.trailingSoftHyphenWidth && hasLeadingTextContent(candidateContent)) { |
| // A trailing soft hyphen with a wrapped text content turns into a visible hyphen. |
| // Let's check if there's enough space for the hyphen character. |
| auto hyphenOverflows = *lineStatus.trailingSoftHyphenWidth > lineStatus.availableWidth; |
| auto action = hyphenOverflows ? Result::Action::RevertToLastNonOverflowingWrapOpportunity : Result::Action::WrapWithHyphen; |
| result = { action, IsEndOfLine::Yes }; |
| } |
| return result; |
| } |
| |
| InlineContentBreaker::Result InlineContentBreaker::processOverflowingContent(const ContinuousContent& overflowContent, const LineStatus& lineStatus) const |
| { |
| auto continuousContent = ContinuousContent { overflowContent }; |
| ASSERT(!continuousContent.runs().isEmpty()); |
| |
| ASSERT(continuousContent.logicalWidth() > lineStatus.availableWidth); |
| if (continuousContent.hasCollapsibleContent()) { |
| if (lineStatus.hasFullyCollapsibleTrailingContent && continuousContent.isFullyCollapsible()) { |
| // If this new content is fully collapsible, it should surely fit. |
| return { Result::Action::Keep, IsEndOfLine::No }; |
| } |
| // Check if the content fits if we collapsed it. |
| auto spaceRequired = continuousContent.logicalWidth() - continuousContent.trailingCollapsibleWidth(); |
| if (lineStatus.hasFullyCollapsibleTrailingContent) |
| spaceRequired -= continuousContent.leadingCollapsibleWidth(); |
| if (spaceRequired <= lineStatus.availableWidth) |
| return { Result::Action::Keep, IsEndOfLine::No }; |
| } else if (lineStatus.collapsibleWidth && isNonContentRunsOnly(continuousContent)) { |
| // Let's see if the non-content runs fit when the line has trailing collapsible content. |
| // "text content <span style="padding: 1px"></span>" <- the <span></span> runs could fit after collapsing the trailing whitespace. |
| if (continuousContent.logicalWidth() <= lineStatus.availableWidth + lineStatus.collapsibleWidth) |
| return { Result::Action::Keep }; |
| } |
| if (isVisuallyEmptyWhitespaceContent(continuousContent) && shouldKeepEndOfLineWhitespace(continuousContent)) { |
| // This overflowing content apparently falls into the remove/hang end-of-line-spaces category. |
| // see https://www.w3.org/TR/css-text-3/#white-space-property matrix |
| return { Result::Action::Keep }; |
| } |
| |
| size_t overflowingRunIndex = 0; |
| if (hasTextRun(continuousContent)) { |
| auto tryBreakingContentWithText = [&]() -> std::optional<Result> { |
| // 1. This text content is not breakable. |
| // 2. This breakable text content does not fit at all. Not even the first glyph. This is a very special case. |
| // 3. We can break the content but it still overflows. |
| // 4. Managed to break the content before the overflow point. |
| auto overflowingContent = processOverflowingContentWithText(continuousContent, lineStatus); |
| overflowingRunIndex = overflowingContent.runIndex; |
| if (!overflowingContent.breakingPosition) |
| return { }; |
| auto trailingContent = overflowingContent.breakingPosition->trailingContent; |
| if (!trailingContent) { |
| // We tried to break the content but the available space can't even accommodate the first glyph. |
| // 1. Wrap the content over to the next line when we've got content on the line already. |
| // 2. Keep the first glyph on the empty line (or keep the whole run if it has only one glyph/completely empty). |
| if (lineStatus.hasContent) |
| return Result { Result::Action::Wrap, IsEndOfLine::Yes }; |
| |
| auto leadingTextRunIndex = *firstTextRunIndex(continuousContent); |
| auto& leadingTextRun = continuousContent.runs()[leadingTextRunIndex]; |
| auto& inlineTextItem = downcast<InlineTextItem>(leadingTextRun.inlineItem); |
| auto firstCodePointLength = [&]() -> size_t { |
| auto textContent = inlineTextItem.inlineTextBox().content(); |
| if (textContent.is8Bit()) |
| return 1; |
| UChar32 character; |
| size_t endOfCodePoint = 0; |
| U16_NEXT(textContent.characters16(), endOfCodePoint, textContent.length(), character); |
| return endOfCodePoint; |
| }(); |
| |
| if (inlineTextItem.length() <= firstCodePointLength) { |
| if (continuousContent.runs().size() == 1) { |
| // Let's return single, leading text items as is. |
| return Result { Result::Action::Keep, IsEndOfLine::Yes }; |
| } |
| return Result { Result::Action::Break, IsEndOfLine::Yes, Result::PartialTrailingContent { leadingTextRunIndex, { } } }; |
| } |
| |
| auto firstCodePointWidth = TextUtil::width(inlineTextItem, leadingTextRun.style.fontCascade(), inlineTextItem.start(), inlineTextItem.start() + firstCodePointLength, lineStatus.contentLogicalRight); |
| return Result { Result::Action::Break, IsEndOfLine::Yes, Result::PartialTrailingContent { leadingTextRunIndex, PartialRun { firstCodePointLength, firstCodePointWidth } } }; |
| } |
| if (trailingContent->overflows && lineStatus.hasContent) { |
| // We managed to break a run with overflow but the line already has content. Let's wrap it to the next line. |
| return Result { Result::Action::Wrap, IsEndOfLine::Yes }; |
| } |
| // Either we managed to break with no overflow or the line is empty. |
| auto trailingPartialContent = Result::PartialTrailingContent { overflowingContent.breakingPosition->runIndex, trailingContent->partialRun }; |
| return Result { Result::Action::Break, IsEndOfLine::Yes, trailingPartialContent }; |
| }; |
| if (auto result = tryBreakingContentWithText()) |
| return *result; |
| } else if (continuousContent.runs().size() > 1) { |
| // FIXME: Add support for various content. |
| auto& runs = continuousContent.runs(); |
| for (size_t i = 0; i < runs.size(); ++i) { |
| if (runs[i].inlineItem.isBox()) { |
| overflowingRunIndex = i; |
| break; |
| } |
| } |
| } |
| |
| // If we are not allowed to break this overflowing content, we still need to decide whether keep it or wrap it to the next line. |
| if (!lineStatus.hasContent) |
| return { Result::Action::Keep, IsEndOfLine::No }; |
| // Now either wrap this content over to the next line or revert back to an earlier wrapping opportunity, or not wrap at all. |
| auto shouldWrapUnbreakableContentToNextLine = [&] { |
| // The individual runs in this continuous content don't break, let's check if we are allowed to wrap this content to next line (e.g. pre would prevent us from wrapping). |
| // Parent style drives the wrapping behavior here. |
| // e.g. <div style="white-space: nowrap">some text<div style="display: inline-block; white-space: pre-wrap"></div></div>. |
| // While the inline-block has pre-wrap which allows wrapping, the content lives in a nowrap context. |
| return TextUtil::isWrappingAllowed(continuousContent.runs()[overflowingRunIndex].style); |
| }; |
| if (shouldWrapUnbreakableContentToNextLine()) |
| return { Result::Action::Wrap, IsEndOfLine::Yes }; |
| if (lineStatus.hasWrapOpportunityAtPreviousPosition) |
| return { Result::Action::RevertToLastWrapOpportunity, IsEndOfLine::Yes }; |
| return { Result::Action::Keep, IsEndOfLine::No }; |
| } |
| |
| static std::optional<size_t> findTrailingRunIndex(const InlineContentBreaker::ContinuousContent::RunList& runs, size_t breakableRunIndex) |
| { |
| // When the breaking position is at the beginning of the run, the trailing run is the previous one. |
| if (!breakableRunIndex) |
| return { }; |
| // Try not break content at inline box boundary |
| // e.g. <span>fits</span><span>overflows</span> |
| // when the text "overflows" completely overflows, let's break the content right before the '<span>'. |
| auto trailingCandidateIndex = breakableRunIndex - 1; |
| auto isAtInlineBox = runs[trailingCandidateIndex].inlineItem.isInlineBoxStart(); |
| return !isAtInlineBox ? trailingCandidateIndex : trailingCandidateIndex ? std::make_optional(trailingCandidateIndex - 1) : std::nullopt; |
| } |
| |
| static bool isWrappableRun(const InlineContentBreaker::ContinuousContent::Run& run) |
| { |
| ASSERT(run.inlineItem.isText() || run.inlineItem.isInlineBoxStart() || run.inlineItem.isInlineBoxEnd() || run.inlineItem.layoutBox().isImage()); |
| if (!run.inlineItem.isText()) { |
| // Can't break horizontal spacing -> e.g. <span style="padding-right: 100px;">textcontent</span>, if the [inline box end] is the overflown inline item |
| // we need to check if there's another inline item beyond the [inline box end] to split. |
| return false; |
| } |
| // Check if this text run needs to stay on the current line. |
| return TextUtil::isWrappingAllowed(run.style); |
| } |
| |
| static inline bool canBreakBefore(UChar32 character, LineBreak lineBreak) |
| { |
| // FIXME: This should include all the cases from https://unicode.org/reports/tr14 |
| // Use a breaking matrix similar to lineBreakTable in BreakLines.cpp |
| // Also see kBreakAllLineBreakClassTable in third_party/blink/renderer/platform/text/text_break_iterator.cc |
| if (lineBreak != LineBreak::Loose) { |
| // The following breaks are allowed for loose line breaking if the preceding character belongs to the Unicode |
| // line breaking class ID, and are otherwise forbidden: |
| // ‐ U+2010, – U+2013 |
| // https://drafts.csswg.org/css-text/#line-break-property |
| if (character == hyphen || character == enDash) |
| return false; |
| } |
| if (character == noBreakSpace) |
| return false; |
| auto isPunctuation = U_GET_GC_MASK(character) & (U_GC_PS_MASK | U_GC_PE_MASK | U_GC_PI_MASK | U_GC_PF_MASK | U_GC_PO_MASK); |
| return character == reverseSolidus || !isPunctuation; |
| } |
| |
| static inline std::optional<size_t> lastValidBreakingPosition(const InlineContentBreaker::ContinuousContent::RunList& runs, size_t textRunIndex) |
| { |
| auto& textRun = runs[textRunIndex]; |
| auto& inlineTextItem = downcast<InlineTextItem>(textRun.inlineItem); |
| ASSERT(inlineTextItem.length()); |
| auto lineBreak = textRun.style.lineBreak(); |
| |
| auto adjactentTextRunIndex = nextTextRunIndex(runs, textRunIndex); |
| if (!adjactentTextRunIndex) |
| return inlineTextItem.end(); |
| |
| auto& nextInlineTextItem = downcast<InlineTextItem>(runs[*adjactentTextRunIndex].inlineItem); |
| auto canBreakAtRunBoundary = nextInlineTextItem.isWhitespace() ? nextInlineTextItem.style().whiteSpace() != WhiteSpace::BreakSpaces : |
| canBreakBefore(nextInlineTextItem.inlineTextBox().content()[nextInlineTextItem.start()], lineBreak); |
| if (canBreakAtRunBoundary) |
| return inlineTextItem.end(); |
| |
| // Find out if the candidate position for arbitrary breaking is valid. We can't always break between any characters. |
| auto text = inlineTextItem.inlineTextBox().content(); |
| auto left = inlineTextItem.start(); |
| for (auto index = inlineTextItem.end() - 1; index > left; --index) { |
| U16_SET_CP_START(text, left, index); |
| // We should never find surrogates/segments across inline items. |
| ASSERT(index > inlineTextItem.start()); |
| if (canBreakBefore(text[index], lineBreak)) |
| return index; |
| } |
| return { }; |
| } |
| |
| static std::optional<TextUtil::WordBreakLeft> midWordBreak(const InlineContentBreaker::ContinuousContent::Run& textRun, InlineLayoutUnit runLogicalLeft, InlineLayoutUnit availableWidth) |
| { |
| ASSERT(textRun.style.wordBreak() == WordBreak::BreakAll); |
| auto& inlineTextItem = downcast<InlineTextItem>(textRun.inlineItem); |
| |
| auto wordBreak = TextUtil::breakWord(inlineTextItem, textRun.style.fontCascade(), textRun.logicalWidth, availableWidth, runLogicalLeft); |
| if (!wordBreak.length || wordBreak.length == inlineTextItem.length()) |
| return { }; |
| |
| // Find out if the candidate position for arbitrary breaking is valid. We can't always break between any characters. |
| auto lineBreak = textRun.style.lineBreak(); |
| auto text = inlineTextItem.inlineTextBox().content(); |
| if (canBreakBefore(text[inlineTextItem.start() + wordBreak.length], lineBreak)) |
| return wordBreak; |
| |
| const auto left = inlineTextItem.start(); |
| auto right = left + wordBreak.length; |
| for (; right > left; --right) { |
| U16_SET_CP_START(text, left, right); |
| if (canBreakBefore(text[right], lineBreak)) |
| break; |
| } |
| if (left == right) |
| return { }; |
| return TextUtil::WordBreakLeft { right - left, TextUtil::width(inlineTextItem, textRun.style.fontCascade(), left, right, runLogicalLeft) }; |
| } |
| |
| struct CandidateTextRunForBreaking { |
| size_t index { 0 }; |
| bool isOverflowingRun { true }; |
| InlineLayoutUnit logicalLeft { 0 }; |
| }; |
| std::optional<InlineContentBreaker::PartialRun> InlineContentBreaker::tryBreakingTextRun(const ContinuousContent::RunList& runs, const CandidateTextRunForBreaking& candidateTextRun, InlineLayoutUnit availableWidth, bool lineHasWrapOpportunityAtPreviousPosition) const |
| { |
| auto& candidateRun = runs[candidateTextRun.index]; |
| ASSERT(candidateRun.inlineItem.isText()); |
| auto& inlineTextItem = downcast<InlineTextItem>(candidateRun.inlineItem); |
| auto& style = candidateRun.style; |
| auto lineHasRoomForContent = availableWidth > 0; |
| |
| auto breakRules = wordBreakBehavior(style, lineHasWrapOpportunityAtPreviousPosition); |
| if (breakRules.isEmpty()) |
| return { }; |
| |
| auto& fontCascade = style.fontCascade(); |
| if (breakRules.contains(WordBreakRule::AtArbitraryPositionWithinWords)) { |
| auto tryBreakingAtArbitraryPositionWithinWords = [&]() -> std::optional<PartialRun> { |
| // Breaking is allowed within “words”: specifically, in addition to soft wrap opportunities allowed for normal, any typographic letter units |
| // It does not affect rules governing the soft wrap opportunities created by white space. Hyphenation is not applied. |
| ASSERT(!breakRules.contains(WordBreakRule::AtHyphenationOpportunities)); |
| if (inlineTextItem.isWhitespace()) { |
| // AtArbitraryPositionWithinWords does not affect the breaking opportunities around whitespace. |
| return { }; |
| } |
| |
| if (!inlineTextItem.length()) { |
| // Empty/single character text runs may be breakable based on style, but in practice we can't really split them any further. |
| return { }; |
| } |
| |
| if (candidateTextRun.isOverflowingRun) { |
| if (lineHasRoomForContent) { |
| // Try to break the overflowing run mid-word. |
| if (auto wordBreak = midWordBreak(candidateRun, candidateTextRun.logicalLeft, availableWidth)) |
| return PartialRun { wordBreak->length, wordBreak->logicalWidth }; |
| } |
| if (canBreakBefore(inlineTextItem.inlineTextBox().content()[inlineTextItem.start()], style.lineBreak())) |
| return PartialRun { }; |
| return { }; |
| } |
| |
| // This is a non-overflowing content. |
| ASSERT(lineHasRoomForContent); |
| if (auto breakingPosition = lastValidBreakingPosition(runs, candidateTextRun.index)) { |
| ASSERT(*breakingPosition <= inlineTextItem.end()); |
| auto trailingLength = *breakingPosition - inlineTextItem.start(); |
| auto startPosition = inlineTextItem.start(); |
| auto endPosition = startPosition + trailingLength; |
| return PartialRun { trailingLength, TextUtil::width(inlineTextItem, fontCascade, startPosition, endPosition, candidateTextRun.logicalLeft) }; |
| } |
| return { }; |
| }; |
| return tryBreakingAtArbitraryPositionWithinWords(); |
| } |
| |
| if (breakRules.contains(WordBreakRule::AtHyphenationOpportunities)) { |
| auto tryBreakingAtHyphenationOpportunity = [&]() -> std::optional<PartialRun> { |
| // Find the hyphen position as follows: |
| // 1. Split the text by taking the hyphen width into account |
| // 2. Find the last hyphen position before the split position |
| if (candidateTextRun.isOverflowingRun && !lineHasRoomForContent) { |
| // We won't be able to find hyphen location when there's no available space. |
| return { }; |
| } |
| auto runLength = inlineTextItem.length(); |
| unsigned limitBefore = style.hyphenationLimitBefore() == RenderStyle::initialHyphenationLimitBefore() ? 0 : style.hyphenationLimitBefore(); |
| unsigned limitAfter = style.hyphenationLimitAfter() == RenderStyle::initialHyphenationLimitAfter() ? 0 : style.hyphenationLimitAfter(); |
| // Check if this run can accommodate the before/after limits at all before start measuring text. |
| if (limitBefore >= runLength || limitAfter >= runLength || limitBefore + limitAfter > runLength) |
| return { }; |
| |
| unsigned leftSideLength = runLength; |
| auto hyphenWidth = InlineLayoutUnit { fontCascade.width(TextRun { StringView { style.hyphenString() } }) }; |
| if (candidateTextRun.isOverflowingRun) { |
| auto availableWidthExcludingHyphen = availableWidth - hyphenWidth; |
| if (availableWidthExcludingHyphen <= 0 || !enoughWidthForHyphenation(availableWidthExcludingHyphen, fontCascade.pixelSize())) |
| return { }; |
| leftSideLength = TextUtil::breakWord(inlineTextItem, fontCascade, candidateRun.logicalWidth, availableWidthExcludingHyphen, candidateTextRun.logicalLeft).length; |
| } |
| if (leftSideLength < limitBefore) |
| return { }; |
| // Adjust before index to accommodate the limit-after value (it's the last potential hyphen location in this run). |
| auto hyphenBefore = std::min(leftSideLength, runLength - limitAfter) + 1; |
| unsigned hyphenLocation = lastHyphenLocation(StringView(inlineTextItem.inlineTextBox().content()).substring(inlineTextItem.start(), inlineTextItem.length()), hyphenBefore, style.computedLocale()); |
| if (!hyphenLocation || hyphenLocation < limitBefore) |
| return { }; |
| // hyphenLocation is relative to the start of this InlineItemText. |
| ASSERT(inlineTextItem.start() + hyphenLocation < inlineTextItem.end()); |
| auto trailingPartialRunWidthWithHyphen = TextUtil::width(inlineTextItem, fontCascade, inlineTextItem.start(), inlineTextItem.start() + hyphenLocation, candidateTextRun.logicalLeft); |
| return PartialRun { hyphenLocation, trailingPartialRunWidthWithHyphen, hyphenWidth }; |
| }; |
| if (auto partialRun = tryBreakingAtHyphenationOpportunity()) |
| return partialRun; |
| } |
| |
| if (breakRules.contains(WordBreakRule::AtArbitraryPosition)) { |
| auto tryBreakingAtArbitraryPosition = [&]() -> PartialRun { |
| if (!inlineTextItem.length()) { |
| // Empty text runs may be breakable based on style, but in practice we can't really split them any further. |
| return { }; |
| } |
| if (!candidateTextRun.isOverflowingRun) { |
| // When the run can be split at arbitrary position let's just return the entire run when it is intended to fit on the line. |
| // However the breaking properties only set rules for text content, so let's check if this run is adjacent to another text run. |
| ASSERT(inlineTextItem.length()); |
| // FIXME: We may need to check if the "next" text run is visually adjacent to this non-overflowing run too (e.g. A<span style="border: 100px solid green;"></span>B) |
| if (nextTextRunIndex(runs, candidateTextRun.index)) { |
| // We are in-between text runs. It's okay to return the entire run triggering split at the very right edge. |
| auto trailingPartialRunWidth = TextUtil::width(inlineTextItem, fontCascade, candidateTextRun.logicalLeft); |
| return { inlineTextItem.length(), trailingPartialRunWidth }; |
| } |
| auto startPosition = inlineTextItem.start() + 1; |
| auto endPosition = inlineTextItem.end(); |
| return { inlineTextItem.length() - 1, TextUtil::width(inlineTextItem, fontCascade, startPosition, endPosition, candidateTextRun.logicalLeft) }; |
| } |
| if (!lineHasRoomForContent) { |
| // Fast path for cases when there's no room at all. The content is breakable but we don't have space for it. |
| return { }; |
| } |
| auto wordBreak = TextUtil::breakWord(inlineTextItem, fontCascade, candidateRun.logicalWidth, availableWidth, candidateTextRun.logicalLeft); |
| return { wordBreak.length, wordBreak.logicalWidth }; |
| }; |
| // With arbitrary breaking there's always a valid breaking position (even if it is before the first position). |
| return tryBreakingAtArbitraryPosition(); |
| } |
| |
| return { }; |
| } |
| |
| std::optional<InlineContentBreaker::OverflowingTextContent::BreakingPosition> InlineContentBreaker::tryBreakingOverflowingRun(const LineStatus& lineStatus, const ContinuousContent::RunList& runs, size_t overflowingRunIndex, InlineLayoutUnit nonOverflowingContentWidth) const |
| { |
| auto overflowingRun = runs[overflowingRunIndex]; |
| if (!isWrappableRun(overflowingRun)) |
| return { }; |
| |
| auto avilableWidth = std::max(0.0f, lineStatus.availableWidth - nonOverflowingContentWidth); |
| auto partialOverflowingRun = tryBreakingTextRun(runs, { overflowingRunIndex, true, lineStatus.contentLogicalRight + nonOverflowingContentWidth }, avilableWidth, lineStatus.hasWrapOpportunityAtPreviousPosition); |
| if (!partialOverflowingRun) |
| return { }; |
| if (partialOverflowingRun->length) |
| return OverflowingTextContent::BreakingPosition { overflowingRunIndex, OverflowingTextContent::BreakingPosition::TrailingContent { false, partialOverflowingRun } }; |
| // When the breaking position is at the beginning of the run, the trailing run is the previous one. |
| if (auto trailingRunIndex = findTrailingRunIndex(runs, overflowingRunIndex)) |
| return OverflowingTextContent::BreakingPosition { *trailingRunIndex, OverflowingTextContent::BreakingPosition::TrailingContent { } }; |
| // Sometimes we can't accommodate even the very first character. |
| // Note that this is different from when there's no breakable run in this set. |
| return OverflowingTextContent::BreakingPosition { }; |
| } |
| |
| std::optional<InlineContentBreaker::OverflowingTextContent::BreakingPosition> InlineContentBreaker::tryBreakingPreviousNonOverflowingRuns(const LineStatus& lineStatus, const ContinuousContent::RunList& runs, size_t overflowingRunIndex, InlineLayoutUnit nonOverflowingContentWidth) const |
| { |
| auto previousContentWidth = nonOverflowingContentWidth; |
| for (auto index = overflowingRunIndex; index--;) { |
| auto& run = runs[index]; |
| previousContentWidth -= run.logicalWidth; |
| if (!isWrappableRun(run)) |
| continue; |
| ASSERT(run.inlineItem.isText()); |
| auto avilableWidth = std::max(0.0f, lineStatus.availableWidth - previousContentWidth); |
| if (auto partialRun = tryBreakingTextRun(runs, { index, false, lineStatus.contentLogicalRight + previousContentWidth }, avilableWidth, lineStatus.hasWrapOpportunityAtPreviousPosition)) { |
| // We know this run fits, so if breaking is allowed on the run, it should return a non-empty left-side |
| // since it's either at hyphen position or the entire run is returned. |
| ASSERT(partialRun->length); |
| auto runIsFullyAccommodated = partialRun->length == downcast<InlineTextItem>(run.inlineItem).length(); |
| if (runIsFullyAccommodated) { |
| auto trailingRunIndex = [&] { |
| // Try not break content at inline box boundary. |
| // e.g. <span style="word-wrap: break-word">fits_and_we_break_at_the_right_edge</span><span>overflows</span> |
| // we should forward the breaking index to the closing inline box. |
| // FIXME: We may wanna skip over the visually empty inline boxes only e.g. <span style="word-wrap: break-word">fits_and_we_break_at_the_right_edge</span><span></span><span>overflows</span> |
| auto trailingInlineBoxEndIndex = std::optional<size_t> { }; |
| for (auto candidateIndex = index + 1; candidateIndex < overflowingRunIndex; ++candidateIndex) { |
| auto& trailingInlineItem = runs[candidateIndex].inlineItem; |
| if (trailingInlineItem.isInlineBoxEnd()) |
| trailingInlineBoxEndIndex = candidateIndex; |
| if (!trailingInlineItem.isInlineBoxStart() && !trailingInlineItem.isInlineBoxEnd()) |
| break; |
| } |
| ASSERT(!trailingInlineBoxEndIndex || *trailingInlineBoxEndIndex < overflowingRunIndex); |
| return trailingInlineBoxEndIndex.value_or(index); |
| }; |
| return OverflowingTextContent::BreakingPosition { trailingRunIndex(), OverflowingTextContent::BreakingPosition::TrailingContent { false, std::nullopt } }; |
| } |
| return OverflowingTextContent::BreakingPosition { index, OverflowingTextContent::BreakingPosition::TrailingContent { false, partialRun } }; |
| } |
| } |
| return { }; |
| } |
| |
| std::optional<InlineContentBreaker::OverflowingTextContent::BreakingPosition> InlineContentBreaker::tryBreakingNextOverflowingRuns(const LineStatus& lineStatus, const ContinuousContent::RunList& runs, size_t overflowingRunIndex, InlineLayoutUnit nonOverflowingContentWidth) const |
| { |
| auto nextContentWidth = nonOverflowingContentWidth + runs[overflowingRunIndex].logicalWidth; |
| for (auto index = overflowingRunIndex + 1; index < runs.size(); ++index) { |
| auto& run = runs[index]; |
| if (!isWrappableRun(run)) { |
| nextContentWidth += run.logicalWidth; |
| continue; |
| } |
| ASSERT(run.inlineItem.isText()); |
| // At this point the available space is zero. Let's try the break these overflowing set of runs at the earliest possible. |
| if (auto partialRun = tryBreakingTextRun(runs, { index, true, lineStatus.contentLogicalRight + nextContentWidth }, 0, lineStatus.hasWrapOpportunityAtPreviousPosition)) { |
| // <span>unbreakable_and_overflows<span style="word-break: break-all">breakable</span> |
| // The partial run length could very well be 0 meaning the trailing run is actually the overflowing run (see above in the example). |
| if (partialRun->length) { |
| // We managed to break this text run mid content. It has to be either an arbitrary mid-word or a hyphen break. |
| return OverflowingTextContent::BreakingPosition { index, OverflowingTextContent::BreakingPosition::TrailingContent { true, partialRun } }; |
| } |
| if (auto trailingRunIndex = findTrailingRunIndex(runs, index)) { |
| // At worst we are back to the overflowing run, like in the example above. |
| ASSERT(*trailingRunIndex >= overflowingRunIndex); |
| return OverflowingTextContent::BreakingPosition { *trailingRunIndex, OverflowingTextContent::BreakingPosition::TrailingContent { true } }; |
| } |
| // This happens when the overflowing run is also the first run in this set, no trailing run. |
| return OverflowingTextContent::BreakingPosition { overflowingRunIndex, { } }; |
| } |
| nextContentWidth += run.logicalWidth; |
| } |
| return { }; |
| } |
| |
| InlineContentBreaker::OverflowingTextContent InlineContentBreaker::processOverflowingContentWithText(const ContinuousContent& continuousContent, const LineStatus& lineStatus) const |
| { |
| auto& runs = continuousContent.runs(); |
| ASSERT(!runs.isEmpty()); |
| |
| // Check where the overflow occurs and use the corresponding style to figure out the breaking behavior. |
| // <span style="word-break: normal">first</span><span style="word-break: break-all">second</span><span style="word-break: normal">third</span> |
| |
| // First find the overflowing run. |
| auto nonOverflowingContentWidth = InlineLayoutUnit { }; |
| auto overflowingRunIndex = runs.size(); |
| for (size_t index = 0; index < runs.size(); ++index) { |
| auto runLogicalWidth = runs[index].logicalWidth; |
| if (nonOverflowingContentWidth + runLogicalWidth > lineStatus.availableWidth) { |
| overflowingRunIndex = index; |
| break; |
| } |
| nonOverflowingContentWidth += runLogicalWidth; |
| } |
| // We have to have an overflowing run. |
| RELEASE_ASSERT(overflowingRunIndex < runs.size()); |
| |
| // Check first if we can actually break the overflowing run. |
| if (auto breakingPosition = tryBreakingOverflowingRun(lineStatus, runs, overflowingRunIndex, nonOverflowingContentWidth)) |
| return { overflowingRunIndex, breakingPosition }; |
| |
| // We did not manage to break the run that overflows the line. |
| // Let's try to find a previous breaking position starting from the overflowing run. It surely fits. |
| if (auto breakingPosition = tryBreakingPreviousNonOverflowingRuns(lineStatus, runs, overflowingRunIndex, nonOverflowingContentWidth)) |
| return { overflowingRunIndex, breakingPosition }; |
| |
| // At this point we know that there's no breakable run all the way to the overflowing run. |
| // Now we need to check if any run after the overflowing content can break. |
| // e.g. <span>this_content_overflows_but_not_breakable<span><span style="word-break: break-all">but_this_is_breakable</span> |
| if (auto breakingPosition = tryBreakingNextOverflowingRuns(lineStatus, runs, overflowingRunIndex, nonOverflowingContentWidth)) |
| return { overflowingRunIndex, breakingPosition }; |
| |
| // Give up, there's no breakable run in here. |
| return { overflowingRunIndex }; |
| } |
| |
| OptionSet<InlineContentBreaker::WordBreakRule> InlineContentBreaker::wordBreakBehavior(const RenderStyle& style, bool hasWrapOpportunityAtPreviousPosition) const |
| { |
| // Disregard any prohibition against line breaks mandated by the word-break property. |
| // The different wrapping opportunities must not be prioritized. |
| // Note hyphenation is not applied. |
| if (style.lineBreak() == LineBreak::Anywhere) |
| return { WordBreakRule::AtArbitraryPosition }; |
| |
| // Breaking is allowed within “words”. |
| if (style.wordBreak() == WordBreak::BreakAll) |
| return { WordBreakRule::AtArbitraryPositionWithinWords }; |
| |
| auto includeHyphenationIfAllowed = [&](std::optional<InlineContentBreaker::WordBreakRule> wordBreakRule) -> OptionSet<InlineContentBreaker::WordBreakRule> { |
| auto hyphenationIsAllowed = !n_hyphenationIsDisabled && style.hyphens() == Hyphens::Auto && canHyphenate(style.computedLocale()); |
| if (hyphenationIsAllowed) { |
| if (wordBreakRule) |
| return { *wordBreakRule, WordBreakRule::AtHyphenationOpportunities }; |
| return { WordBreakRule::AtHyphenationOpportunities }; |
| } |
| if (wordBreakRule) |
| return *wordBreakRule; |
| return { }; |
| }; |
| |
| // For compatibility with legacy content, the word-break property also supports a deprecated break-word keyword. |
| // When specified, this has the same effect as word-break: normal and overflow-wrap: anywhere, regardless of the actual value of the overflow-wrap property. |
| if (style.wordBreak() == WordBreak::BreakWord && !hasWrapOpportunityAtPreviousPosition) |
| return includeHyphenationIfAllowed(WordBreakRule::AtArbitraryPosition); |
| // OverflowWrap::BreakWord/Anywhere An otherwise unbreakable sequence of characters may be broken at an arbitrary point if there are no otherwise-acceptable break points in the line. |
| // Note that this applies to content where CSS properties (e.g. WordBreak::KeepAll) make it unbreakable. |
| // Soft wrap opportunities introduced by overflow-wrap/word-wrap: break-word are not considered when calculating min-content intrinsic sizes. |
| auto overflowWrapBreakWordIsApplicable = !isInIntrinsicWidthMode(); |
| if (((overflowWrapBreakWordIsApplicable && style.overflowWrap() == OverflowWrap::BreakWord) || style.overflowWrap() == OverflowWrap::Anywhere) && !hasWrapOpportunityAtPreviousPosition) |
| return includeHyphenationIfAllowed(WordBreakRule::AtArbitraryPosition); |
| // Breaking is forbidden within “words”. |
| if (style.wordBreak() == WordBreak::KeepAll) |
| return { }; |
| return includeHyphenationIfAllowed({ }); |
| } |
| |
| void InlineContentBreaker::ContinuousContent::append(const InlineItem& inlineItem, const RenderStyle& style, InlineLayoutUnit logicalWidth, std::optional<InlineLayoutUnit> collapsibleWidth) |
| { |
| ASSERT(inlineItem.isText() || inlineItem.isBox() || inlineItem.isInlineBoxStart() || inlineItem.isInlineBoxEnd()); |
| auto isLeadingCollapsible = collapsibleWidth && (m_runs.isEmpty() || isFullyCollapsible()); |
| m_runs.append({ inlineItem, style, logicalWidth }); |
| m_logicalWidth = clampTo<InlineLayoutUnit>(m_logicalWidth + logicalWidth); |
| if (!collapsibleWidth) { |
| if (inlineItem.isText() || inlineItem.isBox()) { |
| // Inline boxes do not prevent the trailing content from getting collapsed. |
| m_trailingCollapsibleWidth = { }; |
| } |
| return; |
| } |
| ASSERT(*collapsibleWidth <= logicalWidth); |
| if (isLeadingCollapsible) { |
| ASSERT(!m_trailingCollapsibleWidth); |
| m_leadingCollapsibleWidth += *collapsibleWidth; |
| return; |
| } |
| m_trailingCollapsibleWidth = *collapsibleWidth == logicalWidth ? m_trailingCollapsibleWidth + logicalWidth : *collapsibleWidth; |
| } |
| |
| void InlineContentBreaker::ContinuousContent::reset() |
| { |
| m_logicalWidth = { }; |
| m_leadingCollapsibleWidth = { }; |
| m_trailingCollapsibleWidth = { }; |
| m_runs.clear(); |
| } |
| } |
| } |
| #endif |