summaryrefslogtreecommitdiff
path: root/Source/WebCore/rendering/SimpleLineLayout.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Source/WebCore/rendering/SimpleLineLayout.cpp')
-rw-r--r--Source/WebCore/rendering/SimpleLineLayout.cpp1093
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);
}