diff options
Diffstat (limited to 'Source/WebCore/rendering/SimpleLineLayout.cpp')
-rw-r--r-- | Source/WebCore/rendering/SimpleLineLayout.cpp | 1093 |
1 files changed, 766 insertions, 327 deletions
diff --git a/Source/WebCore/rendering/SimpleLineLayout.cpp b/Source/WebCore/rendering/SimpleLineLayout.cpp index b0f0b31af..454e71e5c 100644 --- a/Source/WebCore/rendering/SimpleLineLayout.cpp +++ b/Source/WebCore/rendering/SimpleLineLayout.cpp @@ -33,440 +33,879 @@ #include "HitTestLocation.h" #include "HitTestRequest.h" #include "HitTestResult.h" +#include "Hyphenation.h" #include "InlineTextBox.h" #include "LineWidth.h" +#include "Logging.h" #include "PaintInfo.h" #include "RenderBlockFlow.h" +#include "RenderChildIterator.h" +#include "RenderLineBreak.h" #include "RenderStyle.h" #include "RenderText.h" #include "RenderTextControl.h" -#include "RenderView.h" #include "Settings.h" +#include "SimpleLineLayoutFlowContents.h" #include "SimpleLineLayoutFunctions.h" +#include "SimpleLineLayoutTextFragmentIterator.h" #include "Text.h" #include "TextPaintStyle.h" -#include "break_lines.h" -#include <wtf/unicode/Unicode.h> namespace WebCore { namespace SimpleLineLayout { +#ifndef NDEBUG +#define SET_REASON_AND_RETURN_IF_NEEDED(reason, reasons, includeReasons) { \ + reasons |= reason; \ + if (includeReasons == IncludeReasons::First) \ + return reasons; \ + } +#else +#define SET_REASON_AND_RETURN_IF_NEEDED(reason, reasons, includeReasons) { \ + ASSERT_UNUSED(includeReasons, includeReasons == IncludeReasons::First); \ + reasons |= reason; \ + return reasons; \ + } +#endif + + +template <typename CharacterType> AvoidanceReasonFlags canUseForCharacter(CharacterType, bool textIsJustified, IncludeReasons); + +template<> AvoidanceReasonFlags canUseForCharacter(UChar character, bool textIsJustified, IncludeReasons includeReasons) +{ + AvoidanceReasonFlags reasons = { }; + if (textIsJustified) { + // Include characters up to Latin Extended-B and some punctuation range when text is justified. + bool isLatinIncludingExtendedB = character <= 0x01FF; + bool isPunctuationRange = character >= 0x2010 && character <= 0x2027; + if (!(isLatinIncludingExtendedB || isPunctuationRange)) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasJustifiedNonLatinText, reasons, includeReasons); + } + + if (U16_IS_SURROGATE(character)) + SET_REASON_AND_RETURN_IF_NEEDED(FlowTextHasSurrogatePair, reasons, includeReasons); + + UCharDirection direction = u_charDirection(character); + if (direction == U_RIGHT_TO_LEFT || direction == U_RIGHT_TO_LEFT_ARABIC + || direction == U_RIGHT_TO_LEFT_EMBEDDING || direction == U_RIGHT_TO_LEFT_OVERRIDE + || direction == U_LEFT_TO_RIGHT_EMBEDDING || direction == U_LEFT_TO_RIGHT_OVERRIDE + || direction == U_POP_DIRECTIONAL_FORMAT || direction == U_BOUNDARY_NEUTRAL) + SET_REASON_AND_RETURN_IF_NEEDED(FlowTextHasDirectionCharacter, reasons, includeReasons); + + return reasons; +} + +template<> AvoidanceReasonFlags canUseForCharacter(LChar, bool, IncludeReasons) +{ + return { }; +} + template <typename CharacterType> -static bool canUseForText(const CharacterType* text, unsigned length, const SimpleFontData& fontData) +static AvoidanceReasonFlags canUseForText(const CharacterType* text, unsigned length, const FontCascade& fontCascade, std::optional<float> lineHeightConstraint, + bool textIsJustified, IncludeReasons includeReasons) { - // FIXME: <textarea maxlength=0> generates empty text node. - if (!length) - return false; + AvoidanceReasonFlags reasons = { }; + auto& primaryFont = fontCascade.primaryFont(); + auto& fontMetrics = primaryFont.fontMetrics(); + auto availableSpaceForGlyphAscent = fontMetrics.ascent(); + auto availableSpaceForGlyphDescent = fontMetrics.descent(); + if (lineHeightConstraint) { + auto lineHeightPadding = *lineHeightConstraint - fontMetrics.height(); + availableSpaceForGlyphAscent += lineHeightPadding / 2; + availableSpaceForGlyphDescent += lineHeightPadding / 2; + } + for (unsigned i = 0; i < length; ++i) { - UChar character = text[i]; - if (character == ' ') + auto character = text[i]; + if (FontCascade::treatAsSpace(character)) continue; - // These would be easy to support. - if (character == noBreakSpace) - return false; if (character == softHyphen) - return false; + SET_REASON_AND_RETURN_IF_NEEDED(FlowTextHasSoftHyphen, reasons, includeReasons); - UCharDirection direction = u_charDirection(character); - if (direction == U_RIGHT_TO_LEFT || direction == U_RIGHT_TO_LEFT_ARABIC - || direction == U_RIGHT_TO_LEFT_EMBEDDING || direction == U_RIGHT_TO_LEFT_OVERRIDE - || direction == U_LEFT_TO_RIGHT_EMBEDDING || direction == U_LEFT_TO_RIGHT_OVERRIDE - || direction == U_POP_DIRECTIONAL_FORMAT || direction == U_BOUNDARY_NEUTRAL) - return false; + auto characterReasons = canUseForCharacter(character, textIsJustified, includeReasons); + if (characterReasons != NoReason) + SET_REASON_AND_RETURN_IF_NEEDED(characterReasons, reasons, includeReasons); - if (!fontData.glyphForCharacter(character)) - return false; + auto glyphData = fontCascade.glyphDataForCharacter(character, false); + if (!glyphData.isValid() || glyphData.font != &primaryFont) + SET_REASON_AND_RETURN_IF_NEEDED(FlowPrimaryFontIsInsufficient, reasons, includeReasons); + + if (lineHeightConstraint) { + auto bounds = primaryFont.boundsForGlyph(glyphData.glyph); + if (ceilf(-bounds.y()) > availableSpaceForGlyphAscent || ceilf(bounds.maxY()) > availableSpaceForGlyphDescent) + SET_REASON_AND_RETURN_IF_NEEDED(FlowFontHasOverflowGlyph, reasons, includeReasons); + } } - return true; + return reasons; } -static bool canUseForText(const RenderText& textRenderer, const SimpleFontData& fontData) +static AvoidanceReasonFlags canUseForText(StringView text, const FontCascade& fontCascade, std::optional<float> lineHeightConstraint, bool textIsJustified, IncludeReasons includeReasons) { - if (textRenderer.is8Bit()) - return canUseForText(textRenderer.characters8(), textRenderer.textLength(), fontData); - return canUseForText(textRenderer.characters16(), textRenderer.textLength(), fontData); + if (text.is8Bit()) + return canUseForText(text.characters8(), text.length(), fontCascade, lineHeightConstraint, textIsJustified, includeReasons); + return canUseForText(text.characters16(), text.length(), fontCascade, lineHeightConstraint, textIsJustified, includeReasons); } -bool canUseFor(const RenderBlockFlow& flow) +static AvoidanceReasonFlags canUseForFontAndText(const RenderBlockFlow& flow, IncludeReasons includeReasons) { -#if !PLATFORM(MAC) && !PLATFORM(GTK) && !PLATFORM(EFL) - // FIXME: Non-mac platforms are hitting ASSERT(run.charactersLength() >= run.length()) - // https://bugs.webkit.org/show_bug.cgi?id=123338 - return false; -#else - if (!flow.frame().settings().simpleLineLayoutEnabled()) - return false; - if (!flow.firstChild()) - return false; - // This currently covers <blockflow>#text</blockflow> case. - // The <blockflow><inline>#text</inline></blockflow> case is also popular and should be relatively easy to cover. - if (flow.firstChild() != flow.lastChild()) - return false; - if (!flow.firstChild()->isText()) - return false; - if (!flow.isHorizontalWritingMode()) - return false; - if (flow.flowThreadState() != RenderObject::NotInsideFlowThread) - return false; - if (flow.hasOutline()) - return false; - if (flow.isRubyText() || flow.isRubyBase()) - return false; - if (flow.parent()->isDeprecatedFlexibleBox()) - return false; - // FIXME: Implementation of wrap=hard looks into lineboxes. - if (flow.parent()->isTextArea() && flow.parent()->element()->fastHasAttribute(HTMLNames::wrapAttr)) - return false; - // FIXME: Placeholders do something strange. - if (flow.parent()->isTextControl() && toRenderTextControl(*flow.parent()).textFormControlElement().placeholderElement()) - return false; - // These tests only works during layout. Outside layout this function may give false positives. - if (flow.view().layoutState()) { -#if ENABLE(CSS_SHAPES) - if (flow.view().layoutState()->shapeInsideInfo()) - return false; -#endif - if (flow.view().layoutState()->m_columnInfo) - return false; + AvoidanceReasonFlags reasons = { }; + // We assume that all lines have metrics based purely on the primary font. + const auto& style = flow.style(); + auto& fontCascade = style.fontCascade(); + if (fontCascade.primaryFont().isLoading()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowIsMissingPrimaryFont, reasons, includeReasons); + std::optional<float> lineHeightConstraint; + if (style.lineBoxContain() & LineBoxContainGlyphs) + lineHeightConstraint = lineHeightFromFlow(flow).toFloat(); + bool flowIsJustified = style.textAlign() == JUSTIFY; + for (const auto& textRenderer : childrenOfType<RenderText>(flow)) { + // FIXME: Do not return until after checking all children. + if (!textRenderer.textLength()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowTextIsEmpty, reasons, includeReasons); + if (textRenderer.isCombineText()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowTextIsCombineText, reasons, includeReasons); + if (textRenderer.isCounter()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowTextIsRenderCounter, reasons, includeReasons); + if (textRenderer.isQuote()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowTextIsRenderQuote, reasons, includeReasons); + if (textRenderer.isTextFragment()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowTextIsTextFragment, reasons, includeReasons); + if (textRenderer.isSVGInlineText()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowTextIsSVGInlineText, reasons, includeReasons); + if (!textRenderer.canUseSimpleFontCodePath()) { + // No need to check the code path at this point. We already know it can't be simple. + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasComplexFontCodePath, reasons, includeReasons); + } else { + TextRun run(textRenderer.text()); + run.setCharacterScanForCodePath(false); + if (style.fontCascade().codePath(run) != FontCascade::Simple) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasComplexFontCodePath, reasons, includeReasons); + } + + auto textReasons = canUseForText(textRenderer.stringView(), fontCascade, lineHeightConstraint, flowIsJustified, includeReasons); + if (textReasons != NoReason) + SET_REASON_AND_RETURN_IF_NEEDED(textReasons, reasons, includeReasons); } - const RenderStyle& style = flow.style(); - if (style.textDecorationsInEffect() != TextDecorationNone) - return false; - if (style.textAlign() == JUSTIFY) - return false; + return reasons; +} + +static AvoidanceReasonFlags canUseForStyle(const RenderStyle& style, IncludeReasons includeReasons) +{ + AvoidanceReasonFlags reasons = { }; + if (style.textOverflow()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasTextOverflow, reasons, includeReasons); + if ((style.textDecorationsInEffect() & TextDecorationUnderline) && style.textUnderlinePosition() == TextUnderlinePositionUnder) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasUnsupportedUnderlineDecoration, reasons, includeReasons); // Non-visible overflow should be pretty easy to support. if (style.overflowX() != OVISIBLE || style.overflowY() != OVISIBLE) - return false; - if (!style.textIndent().isZero()) - return false; - if (!style.wordSpacing().isZero() || style.letterSpacing()) - return false; - if (style.textTransform() != TTNONE) - return false; + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasOverflowNotVisible, reasons, includeReasons); if (!style.isLeftToRightDirection()) - return false; - if (style.lineBoxContain() != RenderStyle::initialLineBoxContain()) - return false; + SET_REASON_AND_RETURN_IF_NEEDED(FlowIsNotLTR, reasons, includeReasons); + if (!(style.lineBoxContain() & LineBoxContainBlock)) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasLineBoxContainProperty, reasons, includeReasons); if (style.writingMode() != TopToBottomWritingMode) - return false; + SET_REASON_AND_RETURN_IF_NEEDED(FlowIsNotTopToBottom, reasons, includeReasons); if (style.lineBreak() != LineBreakAuto) - return false; - if (style.wordBreak() != NormalWordBreak) - return false; - if (style.unicodeBidi() != UBNormal || style.rtlOrdering() != LogicalOrder) - return false; - if (style.lineAlign() != LineAlignNone || style.lineSnap() != LineSnapNone) - return false; - if (style.hyphens() == HyphensAuto) - return false; + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasLineBreak, reasons, includeReasons); + if (style.unicodeBidi() != UBNormal) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasNonNormalUnicodeBiDi, reasons, includeReasons); + if (style.rtlOrdering() != LogicalOrder) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasRTLOrdering, reasons, includeReasons); + if (style.lineAlign() != LineAlignNone) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasLineAlignEdges, reasons, includeReasons); + if (style.lineSnap() != LineSnapNone) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasLineSnap, reasons, includeReasons); if (style.textEmphasisFill() != TextEmphasisFillFilled || style.textEmphasisMark() != TextEmphasisMarkNone) - return false; + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasTextEmphasisFillOrMark, reasons, includeReasons); if (style.textShadow()) - return false; -#if ENABLE(CSS_SHAPES) - if (style.resolvedShapeInside()) - return true; -#endif - if (style.textOverflow() || (flow.isAnonymousBlock() && flow.parent()->style().textOverflow())) - return false; - if (style.hasPseudoStyle(FIRST_LINE) || style.hasPseudoStyle(FIRST_LETTER)) - return false; + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasTextShadow, reasons, includeReasons); + if (style.hasPseudoStyle(FIRST_LINE)) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasPseudoFirstLine, reasons, includeReasons); + if (style.hasPseudoStyle(FIRST_LETTER)) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasPseudoFirstLetter, reasons, includeReasons); if (style.hasTextCombine()) - return false; + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasTextCombine, reasons, includeReasons); if (style.backgroundClip() == TextFillBox) - return false; + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasTextFillBox, reasons, includeReasons); if (style.borderFit() == BorderFitLines) - return false; - const RenderText& textRenderer = toRenderText(*flow.firstChild()); - if (flow.containsFloats()) { - // We can't use the code path if any lines would need to be shifted below floats. This is because we don't keep per-line y coordinates. - // It is enough to test the first line width only as currently all floats must be overhanging. - if (textRenderer.minLogicalWidth() > LineWidth(const_cast<RenderBlockFlow&>(flow), false, DoNotIndentText).availableWidth()) - return false; - } - if (textRenderer.isCombineText() || textRenderer.isCounter() || textRenderer.isQuote() || textRenderer.isTextFragment() -#if ENABLE(SVG) - || textRenderer.isSVGInlineText() -#endif - ) - return false; - if (style.font().codePath(TextRun(textRenderer.text())) != Font::Simple) - return false; - if (style.font().isSVGFont()) - return false; - - // We assume that all lines have metrics based purely on the primary font. - auto& primaryFontData = *style.font().primaryFont(); - if (primaryFontData.isLoading()) - return false; - if (!canUseForText(textRenderer, primaryFontData)) - return false; - - return true; + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasBorderFitLines, reasons, includeReasons); + if (style.lineBreak() != LineBreakAuto) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasNonAutoLineBreak, reasons, includeReasons); + if (style.nbspMode() != NBNORMAL) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasWebKitNBSPMode, reasons, includeReasons); +#if ENABLE(CSS_TRAILING_WORD) + if (style.trailingWord() != TrailingWord::Auto) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasNonAutoTrailingWord, reasons, includeReasons); #endif -} - -struct Style { - Style(const RenderStyle& style) - : font(style.font()) - , textAlign(style.textAlign()) - , collapseWhitespace(style.collapseWhiteSpace()) - , preserveNewline(style.preserveNewline()) - , wrapLines(style.autoWrap()) - , breakWordOnOverflow(style.overflowWrap() == BreakOverflowWrap && (wrapLines || preserveNewline)) - , spaceWidth(font.width(TextRun(&space, 1))) - , tabWidth(collapseWhitespace ? 0 : style.tabSize()) - { + if (style.hyphens() == HyphensAuto) { + auto textReasons = canUseForText(style.hyphenString(), style.fontCascade(), std::nullopt, false, includeReasons); + if (textReasons != NoReason) + SET_REASON_AND_RETURN_IF_NEEDED(textReasons, reasons, includeReasons); } - const Font& font; - ETextAlign textAlign; - bool collapseWhitespace; - bool preserveNewline; - bool wrapLines; - bool breakWordOnOverflow; - float spaceWidth; - unsigned tabWidth; -}; - -static inline bool isWhitespace(UChar character, bool preserveNewline) -{ - return character == ' ' || character == '\t' || (!preserveNewline && character == '\n'); + return reasons; } -template <typename CharacterType> -static inline unsigned skipWhitespaces(const CharacterType* text, unsigned offset, unsigned length, bool preserveNewline) +AvoidanceReasonFlags canUseForWithReason(const RenderBlockFlow& flow, IncludeReasons includeReasons) { - for (; offset < length; ++offset) { - if (!isWhitespace(text[offset], preserveNewline)) - return offset; +#ifndef NDEBUG + static std::once_flag onceFlag; + std::call_once(onceFlag, [] { + registerNotifyCallback("com.apple.WebKit.showSimpleLineLayoutCoverage", printSimpleLineLayoutCoverage); + registerNotifyCallback("com.apple.WebKit.showSimpleLineLayoutReasons", printSimpleLineLayoutBlockList); + registerNotifyCallback("com.apple.WebKit.toggleSimpleLineLayout", toggleSimpleLineLayout); + }); +#endif + AvoidanceReasonFlags reasons = { }; + if (!flow.settings().simpleLineLayoutEnabled()) + SET_REASON_AND_RETURN_IF_NEEDED(FeatureIsDisabled, reasons, includeReasons); + if (!flow.parent()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasNoParent, reasons, includeReasons); + if (!flow.firstChild()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasNoChild, reasons, includeReasons); + if (flow.flowThreadState() != RenderObject::NotInsideFlowThread) + SET_REASON_AND_RETURN_IF_NEEDED(FlowIsInsideRegion, reasons, includeReasons); + if (!flow.isHorizontalWritingMode()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasHorizonalWritingMode, reasons, includeReasons); + if (flow.hasOutline()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasOutline, reasons, includeReasons); + if (flow.isRubyText() || flow.isRubyBase()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowIsRuby, reasons, includeReasons); + if (flow.style().hangingPunctuation() != NoHangingPunctuation) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasHangingPunctuation, reasons, includeReasons); + + // Printing does pagination without a flow thread. + if (flow.document().paginated()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowIsPaginated, reasons, includeReasons); + if (flow.firstLineBlock()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasPseudoFirstLine, reasons, includeReasons); + if (flow.isAnonymousBlock() && flow.parent()->style().textOverflow()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasTextOverflow, reasons, includeReasons); + if (flow.parent()->isDeprecatedFlexibleBox()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowIsDepricatedFlexBox, reasons, includeReasons); + // FIXME: Placeholders do something strange. + if (is<RenderTextControl>(*flow.parent()) && downcast<RenderTextControl>(*flow.parent()).textFormControlElement().placeholderElement()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowParentIsPlaceholderElement, reasons, includeReasons); + // FIXME: Implementation of wrap=hard looks into lineboxes. + if (flow.parent()->isTextArea() && flow.parent()->element()->hasAttributeWithoutSynchronization(HTMLNames::wrapAttr)) + SET_REASON_AND_RETURN_IF_NEEDED(FlowParentIsTextAreaWithWrapping, reasons, includeReasons); + // This currently covers <blockflow>#text</blockflow>, <blockflow>#text<br></blockflow> and mutiple (sibling) RenderText cases. + // The <blockflow><inline>#text</inline></blockflow> case is also popular and should be relatively easy to cover. + for (const auto* child = flow.firstChild(); child;) { + if (child->selectionState() != RenderObject::SelectionNone) + SET_REASON_AND_RETURN_IF_NEEDED(FlowChildIsSelected, reasons, includeReasons); + if (is<RenderText>(*child)) { + child = child->nextSibling(); + continue; + } + if (is<RenderLineBreak>(child) && !downcast<RenderLineBreak>(*child).isWBR() && child->style().clear() == CNONE) { + child = child->nextSibling(); + continue; + } + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasNonSupportedChild, reasons, includeReasons); + break; } - return length; + auto styleReasons = canUseForStyle(flow.style(), includeReasons); + if (styleReasons != NoReason) + SET_REASON_AND_RETURN_IF_NEEDED(styleReasons, reasons, includeReasons); + // We can't use the code path if any lines would need to be shifted below floats. This is because we don't keep per-line y coordinates. + if (flow.containsFloats()) { + float minimumWidthNeeded = std::numeric_limits<float>::max(); + for (const auto& textRenderer : childrenOfType<RenderText>(flow)) { + minimumWidthNeeded = std::min(minimumWidthNeeded, textRenderer.minLogicalWidth()); + + for (auto& floatingObject : *flow.floatingObjectSet()) { + ASSERT(floatingObject); + // if a float has a shape, we cannot tell if content will need to be shifted until after we lay it out, + // since the amount of space is not uniform for the height of the float. + if (floatingObject->renderer().shapeOutsideInfo()) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasUnsupportedFloat, reasons, includeReasons); + float availableWidth = flow.availableLogicalWidthForLine(floatingObject->y(), DoNotIndentText); + if (availableWidth < minimumWidthNeeded) + SET_REASON_AND_RETURN_IF_NEEDED(FlowHasUnsupportedFloat, reasons, includeReasons); + } + } + } + auto fontAndTextReasons = canUseForFontAndText(flow, includeReasons); + if (fontAndTextReasons != NoReason) + SET_REASON_AND_RETURN_IF_NEEDED(fontAndTextReasons, reasons, includeReasons); + return reasons; } -template <typename CharacterType> -static float textWidth(const RenderText& renderText, const CharacterType* text, unsigned textLength, unsigned from, unsigned to, float xPosition, const Style& style) +bool canUseFor(const RenderBlockFlow& flow) { - if (style.font.isFixedPitch() || (!from && to == textLength)) - return renderText.width(from, to - from, style.font, xPosition, nullptr, nullptr); - - TextRun run(text + from, to - from); - run.setXPos(xPosition); - run.setCharactersLength(textLength - from); - run.setTabSize(!!style.tabWidth, style.tabWidth); - - ASSERT(run.charactersLength() >= run.length()); - - return style.font.width(run); + return canUseForWithReason(flow, IncludeReasons::First) == NoReason; } -template <typename CharacterType> -static float measureWord(unsigned start, unsigned end, float lineWidth, const Style& style, const CharacterType* text, unsigned textLength, const RenderText& textRenderer) +static float computeLineLeft(ETextAlign textAlign, float availableWidth, float committedWidth, float logicalLeftOffset) { - if (text[start] == ' ' && end == start + 1) - return style.spaceWidth; - - bool measureWithEndSpace = style.collapseWhitespace && end < textLength && text[end] == ' '; - if (measureWithEndSpace) - ++end; - float width = textWidth(textRenderer, text, textLength, start, end, lineWidth, style); + float remainingWidth = availableWidth - committedWidth; + float left = logicalLeftOffset; + switch (textAlign) { + case LEFT: + case WEBKIT_LEFT: + case TASTART: + return left; + case RIGHT: + case WEBKIT_RIGHT: + case TAEND: + return left + std::max<float>(remainingWidth, 0); + case CENTER: + case WEBKIT_CENTER: + return left + std::max<float>(remainingWidth / 2, 0); + case JUSTIFY: + ASSERT_NOT_REACHED(); + break; + } + ASSERT_NOT_REACHED(); + return 0; +} - return measureWithEndSpace ? width - style.spaceWidth : width; +static void revertAllRunsOnCurrentLine(Layout::RunVector& runs) +{ + while (!runs.isEmpty() && !runs.last().isEndOfLine) + runs.removeLast(); } -template <typename CharacterType> -Vector<Run, 4> createLineRuns(unsigned lineStart, LineWidth& lineWidth, LazyLineBreakIterator& lineBreakIterator, const Style& style, const CharacterType* text, unsigned textLength, const RenderText& textRenderer) +static void revertRuns(Layout::RunVector& runs, unsigned positionToRevertTo, float width) { - Vector<Run, 4> lineRuns; - lineRuns.uncheckedAppend(Run(lineStart, 0)); - - unsigned wordEnd = lineStart; - while (wordEnd < textLength) { - ASSERT(!style.collapseWhitespace || !isWhitespace(text[wordEnd], style.preserveNewline)); - - unsigned wordStart = wordEnd; - - if (style.preserveNewline && text[wordStart] == '\n') { - ++wordEnd; - // FIXME: This creates a dedicated run for newline. This is wasteful and unnecessary but it keeps test results unchanged. - if (wordStart > lineStart) - lineRuns.append(Run(wordStart, lineRuns.last().right)); - lineRuns.last().right = lineRuns.last().left; - lineRuns.last().end = wordEnd; + while (runs.size()) { + auto& lastRun = runs.last(); + if (lastRun.end <= positionToRevertTo) + break; + if (lastRun.start >= positionToRevertTo) { + // Revert this run completely. + width -= (lastRun.logicalRight - lastRun.logicalLeft); + runs.removeLast(); + } else { + lastRun.logicalRight -= width; + width = 0; + lastRun.end = positionToRevertTo; + // Partial removal. break; } + } +} - if (!style.collapseWhitespace && isWhitespace(text[wordStart], style.preserveNewline)) - wordEnd = wordStart + 1; - else - wordEnd = nextBreakablePosition<CharacterType, false>(lineBreakIterator, text, textLength, wordStart + 1); - - bool wordIsPrecededByWhitespace = style.collapseWhitespace && wordStart > lineStart && isWhitespace(text[wordStart - 1], style.preserveNewline); - if (wordIsPrecededByWhitespace) - --wordStart; +class LineState { +public: + void setAvailableWidth(float width) { m_availableWidth = width; } + void setCollapedWhitespaceWidth(float width) { m_collapsedWhitespaceWidth = width; } + void setLogicalLeftOffset(float offset) { m_logicalLeftOffset = offset; } + void setOverflowedFragment(const TextFragmentIterator::TextFragment& fragment) { m_overflowedFragment = fragment; } + void setNeedsAllFragments() + { + ASSERT(!m_fragments); + m_fragments.emplace(); + } - float wordWidth = measureWord(wordStart, wordEnd, lineWidth.committedWidth(), style, text, textLength, textRenderer); + float availableWidth() const { return m_availableWidth; } + float logicalLeftOffset() const { return m_logicalLeftOffset; } + const TextFragmentIterator::TextFragment& overflowedFragment() const { return m_overflowedFragment; } + bool hasTrailingWhitespace() const { return m_lastFragment.type() == TextFragmentIterator::TextFragment::Whitespace; } + TextFragmentIterator::TextFragment lastFragment() const { return m_lastFragment; } + bool isWhitespaceOnly() const { return m_trailingWhitespaceWidth && m_runsWidth == m_trailingWhitespaceWidth; } + bool fits(float extra) const { return m_availableWidth >= m_runsWidth + extra; } + bool firstCharacterFits() const { return m_firstCharacterFits; } + float width() const { return m_runsWidth; } + std::pair<unsigned, bool> expansionOpportunityCount(unsigned from, unsigned to) const + { + ASSERT(m_fragments); + // linebreak runs are special. + if (from == to) + return std::make_pair(0, false); + unsigned expansionOpportunityCount = 0; + auto previousFragmentType = TextFragmentIterator::TextFragment::ContentEnd; + for (const auto& fragment : *m_fragments) { + if (fragment.end() <= from) + continue; + auto currentFragmentType = fragment.type(); + auto expansionOpportunity = this->expansionOpportunity(currentFragmentType, previousFragmentType); + if (expansionOpportunity) + ++expansionOpportunityCount; + previousFragmentType = currentFragmentType; + if (fragment.end() >= to) + return std::make_pair(expansionOpportunityCount, expansionOpportunity); + } + ASSERT_NOT_REACHED(); + return std::make_pair(expansionOpportunityCount, false); + } - lineWidth.addUncommittedWidth(wordWidth); + bool isEmpty() const + { + if (!m_lastFragment.isValid()) + return true; + if (!m_lastCompleteFragment.isEmpty()) + return false; + return m_lastFragment.overlapsToNextRenderer(); + } - if (style.wrapLines) { - // Move to the next line if the current one is full and we have something on it. - if (!lineWidth.fitsOnLine() && lineWidth.committedWidth()) - break; + static inline unsigned endPositionForCollapsedFragment(const TextFragmentIterator::TextFragment& fragment) + { + return fragment.isCollapsed() ? fragment.start() + 1 : fragment.end(); + } - // This is for white-space: pre-wrap which requires special handling for end line whitespace. - if (!style.collapseWhitespace && lineWidth.fitsOnLine() && wordEnd < textLength && isWhitespace(text[wordEnd], style.preserveNewline)) { - // Look ahead to see if the next whitespace would fit. - float whitespaceWidth = textWidth(textRenderer, text, textLength, wordEnd, wordEnd + 1, lineWidth.committedWidth(), style); - if (!lineWidth.fitsOnLineIncludingExtraWidth(whitespaceWidth)) { - // If not eat away the rest of the whitespace on the line. - unsigned whitespaceEnd = skipWhitespaces(text, wordEnd, textLength, style.preserveNewline); - // Include newline to this run too. - if (whitespaceEnd < textLength && text[whitespaceEnd] == '\n') - ++whitespaceEnd; - lineRuns.last().end = whitespaceEnd; - lineRuns.last().right = lineWidth.availableWidth(); - break; + void appendFragmentAndCreateRunIfNeeded(const TextFragmentIterator::TextFragment& fragment, Layout::RunVector& runs) + { + // Adjust end position while collapsing. + unsigned endPosition = endPositionForCollapsedFragment(fragment); + // New line needs new run. + if (!m_runsWidth) { + ASSERT(!m_uncompletedWidth); + runs.append(Run(fragment.start(), endPosition, m_runsWidth, m_runsWidth + fragment.width(), false, fragment.hasHyphen())); + } else { + // Advance last completed fragment when the previous fragment is all set (including multiple parts across renderers) + if ((m_lastFragment.type() != fragment.type()) || !m_lastFragment.overlapsToNextRenderer()) { + m_lastCompleteFragment = m_lastFragment; + m_uncompletedWidth = fragment.width(); + } else + m_uncompletedWidth += fragment.width(); + // Collapse neighbouring whitespace, if they are across multiple renderers and are not collapsed yet. + if (m_lastFragment.isCollapsible() && fragment.isCollapsible()) { + ASSERT(m_lastFragment.isLastInRenderer()); + if (!m_lastFragment.isCollapsed()) { + // Line width needs to be adjusted so that now it takes collapsing into consideration. + m_runsWidth -= (m_lastFragment.width() - m_collapsedWhitespaceWidth); } + // This fragment is collapsed completely. No run is needed. + return; + } + if (m_lastFragment.isLastInRenderer() || m_lastFragment.isCollapsed()) + runs.append(Run(fragment.start(), endPosition, m_runsWidth, m_runsWidth + fragment.width(), false, fragment.hasHyphen())); + else { + Run& lastRun = runs.last(); + lastRun.end = endPosition; + lastRun.logicalRight += fragment.width(); + ASSERT(!lastRun.hasHyphen); + lastRun.hasHyphen = fragment.hasHyphen(); } } + m_runsWidth += fragment.width(); + m_lastFragment = fragment; + if (m_fragments) + (*m_fragments).append(fragment); + + if (fragment.type() == TextFragmentIterator::TextFragment::Whitespace) + m_trailingWhitespaceWidth += fragment.width(); + else { + m_trailingWhitespaceWidth = 0; + m_lastNonWhitespaceFragment = fragment; + } - if (wordStart > lineRuns.last().end) { - // There were more than one consecutive whitespace. - ASSERT(wordIsPrecededByWhitespace); - // Include space to the end of the previous run. - lineRuns.last().end++; - lineRuns.last().right += style.spaceWidth; - // Start a new run on the same line. - lineRuns.append(Run(wordStart + 1, lineRuns.last().right)); + if (!m_firstCharacterFits) + m_firstCharacterFits = fragment.start() + 1 > endPosition || m_runsWidth <= m_availableWidth; + } + + TextFragmentIterator::TextFragment revertToLastCompleteFragment(Layout::RunVector& runs) + { + if (!m_uncompletedWidth) { + ASSERT(m_lastFragment == m_lastCompleteFragment); + return m_lastFragment; } + ASSERT(m_lastFragment.isValid()); + m_runsWidth -= m_uncompletedWidth; + revertRuns(runs, endPositionForCollapsedFragment(m_lastCompleteFragment), m_uncompletedWidth); + m_uncompletedWidth = 0; + ASSERT(m_lastCompleteFragment.isValid()); + return m_lastCompleteFragment; + } - if (!lineWidth.fitsOnLine() && style.breakWordOnOverflow) { - // Backtrack and start measuring character-by-character. - lineWidth.addUncommittedWidth(-lineWidth.uncommittedWidth()); - unsigned splitEnd = wordStart; - for (; splitEnd < wordEnd; ++splitEnd) { - float charWidth = textWidth(textRenderer, text, textLength, splitEnd, splitEnd + 1, 0, style); - lineWidth.addUncommittedWidth(charWidth); - if (!lineWidth.fitsOnLine() && splitEnd > lineStart) - break; - lineWidth.commit(); + void removeTrailingWhitespace(Layout::RunVector& runs) + { + if (m_lastFragment.type() != TextFragmentIterator::TextFragment::Whitespace) + return; + if (m_lastNonWhitespaceFragment) { + auto needsReverting = m_lastNonWhitespaceFragment->end() != m_lastFragment.end(); + // Trailing whitespace fragment might actually have zero length. + ASSERT(needsReverting || !m_trailingWhitespaceWidth); + if (needsReverting) { + revertRuns(runs, m_lastNonWhitespaceFragment->end(), m_trailingWhitespaceWidth); + m_runsWidth -= m_trailingWhitespaceWidth; } - lineRuns.last().end = splitEnd; - lineRuns.last().right = lineWidth.committedWidth(); - // To match line boxes, set single-space-only line width to zero. - if (text[lineRuns.last().start] == ' ' && lineRuns.last().start + 1 == lineRuns.last().end) - lineRuns.last().right = lineRuns.last().left; - break; + m_trailingWhitespaceWidth = 0; + m_lastFragment = *m_lastNonWhitespaceFragment; + return; } + // This line is all whitespace. + revertAllRunsOnCurrentLine(runs); + m_runsWidth = 0; + m_trailingWhitespaceWidth = 0; + // FIXME: Make m_lastFragment optional. + m_lastFragment = TextFragmentIterator::TextFragment(); + } - lineWidth.commit(); +private: + bool expansionOpportunity(TextFragmentIterator::TextFragment::Type currentFragmentType, TextFragmentIterator::TextFragment::Type previousFragmentType) const + { + return (currentFragmentType == TextFragmentIterator::TextFragment::Whitespace + || (currentFragmentType == TextFragmentIterator::TextFragment::NonWhitespace && previousFragmentType == TextFragmentIterator::TextFragment::NonWhitespace)); + } - lineRuns.last().right = lineWidth.committedWidth(); - lineRuns.last().end = wordEnd; + float m_availableWidth { 0 }; + float m_logicalLeftOffset { 0 }; + float m_runsWidth { 0 }; + TextFragmentIterator::TextFragment m_overflowedFragment; + TextFragmentIterator::TextFragment m_lastFragment; + std::optional<TextFragmentIterator::TextFragment> m_lastNonWhitespaceFragment; + TextFragmentIterator::TextFragment m_lastCompleteFragment; + float m_uncompletedWidth { 0 }; + float m_trailingWhitespaceWidth { 0 }; // Use this to remove trailing whitespace without re-mesuring the text. + float m_collapsedWhitespaceWidth { 0 }; + // Having one character on the line does not necessarily mean it actually fits. + // First character of the first fragment might be forced on to the current line even if it does not fit. + bool m_firstCharacterFits { false }; + std::optional<Vector<TextFragmentIterator::TextFragment, 30>> m_fragments; +}; - if (style.collapseWhitespace) - wordEnd = skipWhitespaces(text, wordEnd, textLength, style.preserveNewline); +class FragmentForwardIterator : public std::iterator<std::forward_iterator_tag, unsigned> { +public: + FragmentForwardIterator(unsigned fragmentIndex) + : m_fragmentIndex(fragmentIndex) + { + } - if (!lineWidth.fitsOnLine() && style.wrapLines) { - // The first run on the line overflows. - ASSERT(lineRuns.size() == 1); - break; - } + FragmentForwardIterator& operator++() + { + ++m_fragmentIndex; + return *this; } - return lineRuns; + + bool operator!=(const FragmentForwardIterator& other) const { return m_fragmentIndex != other.m_fragmentIndex; } + bool operator==(const FragmentForwardIterator& other) const { return m_fragmentIndex == other.m_fragmentIndex; } + unsigned operator*() const { return m_fragmentIndex; } + +private: + unsigned m_fragmentIndex { 0 }; +}; + +static FragmentForwardIterator begin(const TextFragmentIterator::TextFragment& fragment) { return FragmentForwardIterator(fragment.start()); } +static FragmentForwardIterator end(const TextFragmentIterator::TextFragment& fragment) { return FragmentForwardIterator(fragment.end()); } + +static bool preWrap(const TextFragmentIterator::Style& style) +{ + return style.wrapLines && !style.collapseWhitespace; +} + +static void removeTrailingWhitespace(LineState& lineState, Layout::RunVector& runs, const TextFragmentIterator& textFragmentIterator) +{ + if (!lineState.hasTrailingWhitespace()) + return; + // Remove collapsed whitespace, or non-collapsed pre-wrap whitespace, unless it's the only content on the line -so removing the whitesapce + // would produce an empty line. + const auto& style = textFragmentIterator.style(); + bool collapseWhitespace = style.collapseWhitespace | preWrap(style); + if (!collapseWhitespace) + return; + if (preWrap(style) && lineState.isWhitespaceOnly()) + return; + lineState.removeTrailingWhitespace(runs); } -static float computeLineLeft(ETextAlign textAlign, const LineWidth& lineWidth) +static void updateLineConstrains(const RenderBlockFlow& flow, LineState& line, const TextFragmentIterator::Style& style, bool isFirstLine) { - float remainingWidth = lineWidth.availableWidth() - lineWidth.committedWidth(); - float left = lineWidth.logicalLeftOffset(); - switch (textAlign) { - case LEFT: - case WEBKIT_LEFT: - case TASTART: - return left; - case RIGHT: - case WEBKIT_RIGHT: - case TAEND: - return left + std::max<float>(remainingWidth, 0); - case CENTER: - case WEBKIT_CENTER: - return left + std::max<float>(remainingWidth / 2, 0); - case JUSTIFY: - break; - } - ASSERT_NOT_REACHED(); - return 0; + bool shouldApplyTextIndent = !flow.isAnonymous() || flow.parent()->firstChild() == &flow; + LayoutUnit height = flow.logicalHeight(); + line.setLogicalLeftOffset(flow.logicalLeftOffsetForLine(height, DoNotIndentText) + (shouldApplyTextIndent && isFirstLine ? flow.textIndentOffset() : LayoutUnit(0))); + float logicalRightOffset = flow.logicalRightOffsetForLine(height, DoNotIndentText); + line.setAvailableWidth(std::max<float>(0, logicalRightOffset - line.logicalLeftOffset())); + if (style.textAlign == JUSTIFY) + line.setNeedsAllFragments(); } -static void adjustRunOffsets(Vector<Run, 4>& lineRuns, float adjustment) +static std::optional<unsigned> hyphenPositionForFragment(unsigned splitPosition, TextFragmentIterator::TextFragment& fragmentToSplit, + const TextFragmentIterator& textFragmentIterator, float availableWidth, bool lineIsEmpty) { - if (!adjustment) - return; - for (unsigned i = 0; i < lineRuns.size(); ++i) { - lineRuns[i].left += adjustment; - lineRuns[i].right += adjustment; + auto& style = textFragmentIterator.style(); + bool shouldHyphenate = style.shouldHyphenate && (!style.hyphenLimitLines || fragmentToSplit.wrappingWithHyphenCounter() < *style.hyphenLimitLines); + if (!shouldHyphenate) + return std::nullopt; + + // FIXME: This is a workaround for webkit.org/b/169613. See maxPrefixWidth computation in tryHyphenating(). + // It does not work properly with non-collapsed leading tabs when font is enlarged. + auto adjustedAvailableWidth = availableWidth - style.hyphenStringWidth; + if (!lineIsEmpty) + adjustedAvailableWidth += style.font.spaceWidth(); + if (!enoughWidthForHyphenation(adjustedAvailableWidth, style.font.pixelSize())) + return std::nullopt; + + // We might be able to fit the hyphen at the split position. + auto splitPositionWithHyphen = splitPosition; + // Find a splitting position where hyphen surely fits. + unsigned start = fragmentToSplit.start(); + auto leftSideWidth = textFragmentIterator.textWidth(start, splitPosition, 0); + while (leftSideWidth + style.hyphenStringWidth > availableWidth) { + if (--splitPositionWithHyphen <= start) + return std::nullopt; // No space for hyphen. + leftSideWidth -= textFragmentIterator.textWidth(splitPositionWithHyphen, splitPositionWithHyphen + 1, 0); } + ASSERT(splitPositionWithHyphen > start); + return textFragmentIterator.lastHyphenPosition(fragmentToSplit, splitPositionWithHyphen + 1); } -template <typename CharacterType> -void createTextRuns(Layout::RunVector& runs, unsigned& lineCount, RenderBlockFlow& flow, RenderText& textRenderer) +static TextFragmentIterator::TextFragment splitFragmentToFitLine(TextFragmentIterator::TextFragment& fragmentToSplit, float availableWidth, bool lineIsEmpty, const TextFragmentIterator& textFragmentIterator) { - const Style style(flow.style()); + // FIXME: add surrogate pair support. + unsigned start = fragmentToSplit.start(); + auto it = std::upper_bound(begin(fragmentToSplit), end(fragmentToSplit), availableWidth, [&textFragmentIterator, start](float availableWidth, unsigned index) { + // FIXME: use the actual left position of the line (instead of 0) to calculated width. It might give false width for tab characters. + return availableWidth < textFragmentIterator.textWidth(start, index + 1, 0); + }); + unsigned splitPosition = (*it); + // Does first character fit this line? + if (splitPosition == start) { + if (lineIsEmpty) + ++splitPosition; + } else if (auto hyphenPosition = hyphenPositionForFragment(splitPosition, fragmentToSplit, textFragmentIterator, availableWidth, lineIsEmpty)) + return fragmentToSplit.splitWithHyphen(*hyphenPosition, textFragmentIterator); + return fragmentToSplit.split(splitPosition, textFragmentIterator); +} - const CharacterType* text = textRenderer.text()->getCharacters<CharacterType>(); - const unsigned textLength = textRenderer.textLength(); +enum PreWrapLineBreakRule { Preserve, Ignore }; - LayoutUnit borderAndPaddingBefore = flow.borderAndPaddingBefore(); - LayoutUnit lineHeight = lineHeightFromFlow(flow); +static TextFragmentIterator::TextFragment consumeLineBreakIfNeeded(const TextFragmentIterator::TextFragment& fragment, TextFragmentIterator& textFragmentIterator, LineState& line, Layout::RunVector& runs, + PreWrapLineBreakRule preWrapLineBreakRule = PreWrapLineBreakRule::Preserve) +{ + if (!fragment.isLineBreak()) + return fragment; + + if (preWrap(textFragmentIterator.style()) && preWrapLineBreakRule != PreWrapLineBreakRule::Ignore) + return fragment; - LazyLineBreakIterator lineBreakIterator(textRenderer.text(), flow.style().locale()); + // <br> always produces a run. (required by testing output) + if (fragment.type() == TextFragmentIterator::TextFragment::HardLineBreak) + line.appendFragmentAndCreateRunIfNeeded(fragment, runs); + return textFragmentIterator.nextTextFragment(); +} - unsigned lineEnd = 0; - while (lineEnd < textLength) { - if (style.collapseWhitespace) - lineEnd = skipWhitespaces(text, lineEnd, textLength, style.preserveNewline); +static TextFragmentIterator::TextFragment skipWhitespaceIfNeeded(const TextFragmentIterator::TextFragment& fragment, TextFragmentIterator& textFragmentIterator) +{ + if (!textFragmentIterator.style().collapseWhitespace) + return fragment; - unsigned lineStart = lineEnd; + TextFragmentIterator::TextFragment firstNonWhitespaceFragment = fragment; + while (firstNonWhitespaceFragment.type() == TextFragmentIterator::TextFragment::Whitespace) + firstNonWhitespaceFragment = textFragmentIterator.nextTextFragment(); + return firstNonWhitespaceFragment; +} - // LineWidth reads the current y position from the flow so keep it updated. - flow.setLogicalHeight(lineHeight * lineCount + borderAndPaddingBefore); - LineWidth lineWidth(flow, false, DoNotIndentText); +static TextFragmentIterator::TextFragment firstFragment(TextFragmentIterator& textFragmentIterator, LineState& currentLine, const LineState& previousLine, Layout::RunVector& runs) +{ + // Handle overflowed fragment from previous line. + TextFragmentIterator::TextFragment firstFragment(previousLine.overflowedFragment()); + + if (firstFragment.isEmpty()) + firstFragment = textFragmentIterator.nextTextFragment(); + else if (firstFragment.type() == TextFragmentIterator::TextFragment::Whitespace && preWrap(textFragmentIterator.style()) && previousLine.firstCharacterFits()) { + // Special overflow pre-wrap whitespace handling: skip the overflowed whitespace (even when style says not-collapsible) if we managed to fit at least one character on the previous line. + firstFragment = textFragmentIterator.nextTextFragment(); + // If skipping the whitespace puts us on a newline, skip the newline too as we already wrapped the line. + firstFragment = consumeLineBreakIfNeeded(firstFragment, textFragmentIterator, currentLine, runs, PreWrapLineBreakRule::Ignore); + } + return skipWhitespaceIfNeeded(firstFragment, textFragmentIterator); +} - auto lineRuns = createLineRuns(lineStart, lineWidth, lineBreakIterator, style, text, textLength, textRenderer); +static void forceFragmentToLine(LineState& line, TextFragmentIterator& textFragmentIterator, Layout::RunVector& runs, const TextFragmentIterator::TextFragment& fragment) +{ + line.appendFragmentAndCreateRunIfNeeded(fragment, runs); + // Check if there are more fragments to add to the current line. + auto nextFragment = textFragmentIterator.nextTextFragment(); + if (fragment.overlapsToNextRenderer()) { + while (true) { + if (nextFragment.type() != fragment.type()) + break; + line.appendFragmentAndCreateRunIfNeeded(nextFragment, runs); + // Does it overlap to the next segment? + if (!nextFragment.overlapsToNextRenderer()) + return; + nextFragment = textFragmentIterator.nextTextFragment(); + } + } + // When the forced fragment is followed by either whitespace and/or line break, consume them too, otherwise we end up with an extra whitespace and/or line break. + nextFragment = skipWhitespaceIfNeeded(nextFragment, textFragmentIterator); + nextFragment = consumeLineBreakIfNeeded(nextFragment, textFragmentIterator, line, runs); + line.setOverflowedFragment(nextFragment); +} - lineEnd = lineRuns.last().end; - if (lineStart == lineEnd) - continue; +static bool createLineRuns(LineState& line, const LineState& previousLine, Layout::RunVector& runs, TextFragmentIterator& textFragmentIterator) +{ + const auto& style = textFragmentIterator.style(); + line.setCollapedWhitespaceWidth(style.font.spaceWidth() + style.wordSpacing); + bool lineCanBeWrapped = style.wrapLines || style.breakFirstWordOnOverflow || style.breakAnyWordOnOverflow; + auto fragment = firstFragment(textFragmentIterator, line, previousLine, runs); + while (fragment.type() != TextFragmentIterator::TextFragment::ContentEnd) { + // Hard linebreak. + if (fragment.isLineBreak()) { + // Add the new line fragment only if there's nothing on the line. (otherwise the extra new line character would show up at the end of the content.) + if (line.isEmpty() || fragment.type() == TextFragmentIterator::TextFragment::HardLineBreak) { + if (style.textAlign == RIGHT || style.textAlign == WEBKIT_RIGHT) + line.removeTrailingWhitespace(runs); + line.appendFragmentAndCreateRunIfNeeded(fragment, runs); + } + break; + } + if (lineCanBeWrapped && !line.fits(fragment.width())) { + // Overflow wrapping behaviour: + // 1. Whitesapce collapse on: whitespace is skipped. Jump to next line. + // 2. Whitespace collapse off: whitespace is wrapped. + // 3. First, non-whitespace fragment is either wrapped or kept on the line. (depends on overflow-wrap) + // 5. Non-whitespace fragment when there's already another fragment on the line either gets wrapped (word-break: break-all) + // or gets pushed to the next line. + bool emptyLine = line.isEmpty(); + // Whitespace fragment. + if (fragment.type() == TextFragmentIterator::TextFragment::Whitespace) { + if (!style.collapseWhitespace) { + // Split the fragment; (modified)fragment stays on this line, overflowedFragment is pushed to next line. + line.setOverflowedFragment(splitFragmentToFitLine(fragment, line.availableWidth() - line.width(), emptyLine, textFragmentIterator)); + line.appendFragmentAndCreateRunIfNeeded(fragment, runs); + } + // When whitespace collapse is on, whitespace that doesn't fit is simply skipped. + break; + } + // Non-whitespace fragment. (!style.wrapLines: bug138102(preserve existing behavior) + if (((emptyLine && style.breakFirstWordOnOverflow) || style.breakAnyWordOnOverflow) || !style.wrapLines) { + // Split the fragment; (modified)fragment stays on this line, overflowedFragment is pushed to next line. + line.setOverflowedFragment(splitFragmentToFitLine(fragment, line.availableWidth() - line.width(), emptyLine, textFragmentIterator)); + line.appendFragmentAndCreateRunIfNeeded(fragment, runs); + break; + } + ASSERT(fragment.type() == TextFragmentIterator::TextFragment::NonWhitespace); + // Find out if this non-whitespace fragment has a hyphen where we can break. + if (style.shouldHyphenate) { + auto fragmentToSplit = fragment; + // Split and check if we actually ended up with a hyphen. + auto overflowFragment = splitFragmentToFitLine(fragmentToSplit, line.availableWidth() - line.width(), emptyLine, textFragmentIterator); + if (fragmentToSplit.hasHyphen()) { + line.setOverflowedFragment(overflowFragment); + line.appendFragmentAndCreateRunIfNeeded(fragmentToSplit, runs); + break; + } + // No hyphen, no split. + } + // Non-breakable non-whitespace first fragment. Add it to the current line. -it overflows though. + if (emptyLine) { + forceFragmentToLine(line, textFragmentIterator, runs, fragment); + break; + } + // Non-breakable non-whitespace fragment when there's already content on the line. Push it to the next line. + ASSERT(line.lastFragment().isValid()); + if (line.lastFragment().overlapsToNextRenderer()) { + // Check if this fragment is a continuation of a previous segment. In such cases, we need to remove them all. + textFragmentIterator.revertToEndOfFragment(line.revertToLastCompleteFragment(runs)); + break; + } + line.setOverflowedFragment(fragment); + break; + } + line.appendFragmentAndCreateRunIfNeeded(fragment, runs); + // Find the next text fragment. + fragment = textFragmentIterator.nextTextFragment(line.width()); + } + return (fragment.type() == TextFragmentIterator::TextFragment::ContentEnd && line.overflowedFragment().isEmpty()) || line.overflowedFragment().type() == TextFragmentIterator::TextFragment::ContentEnd; +} - lineRuns.last().isEndOfLine = true; +static ExpansionBehavior expansionBehavior(bool isAfterExpansion, bool lastRunOnLine) +{ + ExpansionBehavior expansionBehavior; + expansionBehavior = isAfterExpansion ? ForbidLeadingExpansion : AllowLeadingExpansion; + expansionBehavior |= lastRunOnLine ? ForbidTrailingExpansion : AllowTrailingExpansion; + return expansionBehavior; +} - float lineLeft = computeLineLeft(style.textAlign, lineWidth); - adjustRunOffsets(lineRuns, lineLeft); +static void justifyRuns(const LineState& line, Layout::RunVector& runs, unsigned firstRunIndex) +{ + ASSERT(runs.size()); + auto widthToDistribute = line.availableWidth() - line.width(); + if (widthToDistribute <= 0) + return; - for (unsigned i = 0; i < lineRuns.size(); ++i) - runs.append(lineRuns[i]); + auto lastRunIndex = runs.size() - 1; + ASSERT(firstRunIndex <= lastRunIndex); + Vector<std::pair<unsigned, ExpansionBehavior>> expansionOpportunityList; + unsigned expansionOpportunityCountOnThisLine = 0; + auto isAfterExpansion = true; + for (auto i = firstRunIndex; i <= lastRunIndex; ++i) { + const auto& run = runs.at(i); + unsigned opportunityCountInRun = 0; + std::tie(opportunityCountInRun, isAfterExpansion) = line.expansionOpportunityCount(run.start, run.end); + expansionOpportunityList.append(std::make_pair(opportunityCountInRun, expansionBehavior(isAfterExpansion, i == lastRunIndex))); + expansionOpportunityCountOnThisLine += opportunityCountInRun; + } + if (!expansionOpportunityCountOnThisLine) + return; - ++lineCount; + ASSERT(expansionOpportunityList.size() == lastRunIndex - firstRunIndex + 1); + auto expansion = widthToDistribute / expansionOpportunityCountOnThisLine; + float accumulatedExpansion = 0; + for (auto i = firstRunIndex; i <= lastRunIndex; ++i) { + auto& run = runs.at(i); + unsigned opportunityCountInRun; + std::tie(opportunityCountInRun, run.expansionBehavior) = expansionOpportunityList.at(i - firstRunIndex); + run.expansion = opportunityCountInRun * expansion; + run.logicalLeft += accumulatedExpansion; + run.logicalRight += (accumulatedExpansion + run.expansion); + accumulatedExpansion += run.expansion; } } -std::unique_ptr<Layout> create(RenderBlockFlow& flow) +static ETextAlign textAlignForLine(const TextFragmentIterator::Style& style, bool lastLine) { - Layout::RunVector runs; - unsigned lineCount = 0; - - RenderText& textRenderer = toRenderText(*flow.firstChild()); - ASSERT(!textRenderer.firstTextBox()); + // Fallback to LEFT (START) alignment for non-collapsable content and for the last line before a forced break or the end of the block. + auto textAlign = style.textAlign; + if (textAlign == JUSTIFY && (!style.collapseWhitespace || lastLine)) + textAlign = LEFT; + return textAlign; +} - if (textRenderer.is8Bit()) - createTextRuns<LChar>(runs, lineCount, flow, textRenderer); +static void closeLineEndingAndAdjustRuns(LineState& line, Layout::RunVector& runs, std::optional<unsigned> lastRunIndexOfPreviousLine, unsigned& lineCount, + const TextFragmentIterator& textFragmentIterator, bool lastLineInFlow) +{ + if (!runs.size() || (lastRunIndexOfPreviousLine && runs.size() - 1 == lastRunIndexOfPreviousLine.value())) + return; + removeTrailingWhitespace(line, runs, textFragmentIterator); + if (!runs.size()) + return; + // Adjust runs' position by taking line's alignment into account. + const auto& style = textFragmentIterator.style(); + auto firstRunIndex = lastRunIndexOfPreviousLine ? lastRunIndexOfPreviousLine.value() + 1 : 0; + auto lineLogicalLeft = line.logicalLeftOffset(); + auto textAlign = textAlignForLine(style, lastLineInFlow || (line.lastFragment().isValid() && line.lastFragment().type() == TextFragmentIterator::TextFragment::HardLineBreak)); + if (textAlign == JUSTIFY) + justifyRuns(line, runs, firstRunIndex); else - createTextRuns<UChar>(runs, lineCount, flow, textRenderer); + lineLogicalLeft = computeLineLeft(textAlign, line.availableWidth(), line.width(), line.logicalLeftOffset()); + for (auto i = firstRunIndex; i < runs.size(); ++i) { + runs[i].logicalLeft += lineLogicalLeft; + runs[i].logicalRight += lineLogicalLeft; + } + runs.last().isEndOfLine = true; + ++lineCount; +} - textRenderer.clearNeedsLayout(); +static void createTextRuns(Layout::RunVector& runs, RenderBlockFlow& flow, unsigned& lineCount) +{ + LayoutUnit borderAndPaddingBefore = flow.borderAndPaddingBefore(); + LayoutUnit lineHeight = lineHeightFromFlow(flow); + LineState line; + bool isEndOfContent = false; + TextFragmentIterator textFragmentIterator = TextFragmentIterator(flow); + std::optional<unsigned> lastRunIndexOfPreviousLine; + do { + flow.setLogicalHeight(lineHeight * lineCount + borderAndPaddingBefore); + LineState previousLine = line; + line = LineState(); + updateLineConstrains(flow, line, textFragmentIterator.style(), !lineCount); + isEndOfContent = createLineRuns(line, previousLine, runs, textFragmentIterator); + closeLineEndingAndAdjustRuns(line, runs, lastRunIndexOfPreviousLine, lineCount, textFragmentIterator, isEndOfContent); + if (runs.size()) + lastRunIndexOfPreviousLine = runs.size() - 1; + } while (!isEndOfContent); +} +std::unique_ptr<Layout> create(RenderBlockFlow& flow) +{ + unsigned lineCount = 0; + Layout::RunVector runs; + createTextRuns(runs, flow, lineCount); return Layout::create(runs, lineCount); } |