| /* |
| * Copyright (C) 2013-2020 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "LayoutIntegrationCoverage.h" |
| |
| #include "DocumentMarkerController.h" |
| #include "HTMLTextFormControlElement.h" |
| #include "HighlightRegister.h" |
| #include "InlineIterator.h" |
| #include "Logging.h" |
| #include "RenderBlockFlow.h" |
| #include "RenderChildIterator.h" |
| #include "RenderCounter.h" |
| #include "RenderImage.h" |
| #include "RenderInline.h" |
| #include "RenderLineBreak.h" |
| #include "RenderMultiColumnFlow.h" |
| #include "RenderTextControl.h" |
| #include "RenderView.h" |
| #include "RuntimeEnabledFeatures.h" |
| #include "Settings.h" |
| #include <pal/Logging.h> |
| #include <wtf/OptionSet.h> |
| |
| #if ENABLE(LAYOUT_FORMATTING_CONTEXT) |
| |
| #define ALLOW_IMAGES 1 |
| #define ALLOW_ALL_REPLACED 1 |
| #define ALLOW_INLINE_BLOCK 1 |
| #define ALLOW_INLINES 0 |
| |
| #ifndef NDEBUG |
| #define SET_REASON_AND_RETURN_IF_NEEDED(reason, reasons, includeReasons) { \ |
| reasons.add(AvoidanceReason::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.add(AvoidanceReason::reason); \ |
| return reasons; \ |
| } |
| #endif |
| |
| #ifndef NDEBUG |
| #define ADD_REASONS_AND_RETURN_IF_NEEDED(newReasons, reasons, includeReasons) { \ |
| reasons.add(newReasons); \ |
| if (includeReasons == IncludeReasons::First) \ |
| return reasons; \ |
| } |
| #else |
| #define ADD_REASONS_AND_RETURN_IF_NEEDED(newReasons, reasons, includeReasons) { \ |
| ASSERT_UNUSED(includeReasons, includeReasons == IncludeReasons::First); \ |
| reasons.add(newReasons); \ |
| return reasons; \ |
| } |
| #endif |
| |
| namespace WebCore { |
| namespace LayoutIntegration { |
| |
| #ifndef NDEBUG |
| static void printReason(AvoidanceReason reason, TextStream& stream) |
| { |
| switch (reason) { |
| case AvoidanceReason::FlowIsInsideANonMultiColumnThread: |
| stream << "flow is inside a non-multicolumn container"; |
| break; |
| case AvoidanceReason::FlowHasHorizonalWritingMode: |
| stream << "horizontal writing mode"; |
| break; |
| case AvoidanceReason::ContentHasOutline: |
| stream << "outline"; |
| break; |
| case AvoidanceReason::ContentIsRuby: |
| stream << "ruby"; |
| break; |
| case AvoidanceReason::FlowHasHangingPunctuation: |
| stream << "hanging punctuation"; |
| break; |
| case AvoidanceReason::FlowIsPaginated: |
| stream << "paginated"; |
| break; |
| case AvoidanceReason::FlowHasTextOverflow: |
| stream << "text-overflow"; |
| break; |
| case AvoidanceReason::FlowIsDepricatedFlexBox: |
| stream << "depricatedFlexBox"; |
| break; |
| case AvoidanceReason::FlowParentIsPlaceholderElement: |
| stream << "placeholder element"; |
| break; |
| case AvoidanceReason::FlowParentIsTextAreaWithWrapping: |
| stream << "wrapping textarea"; |
| break; |
| case AvoidanceReason::FlowHasNonSupportedChild: |
| stream << "unsupported child renderer"; |
| break; |
| case AvoidanceReason::FlowHasUnsupportedFloat: |
| stream << "complicated float"; |
| break; |
| case AvoidanceReason::FlowHasUnsupportedUnderlineDecoration: |
| stream << "text-underline-position: under"; |
| break; |
| case AvoidanceReason::FlowHasJustifiedNonLatinText: |
| stream << "text-align: justify with non-latin text"; |
| break; |
| case AvoidanceReason::FlowHasOverflowNotVisible: |
| stream << "overflow: hidden | scroll | auto"; |
| break; |
| case AvoidanceReason::FlowHasWebKitNBSPMode: |
| stream << "-webkit-nbsp-mode: space"; |
| break; |
| case AvoidanceReason::FlowIsNotLTR: |
| stream << "dir is not LTR"; |
| break; |
| case AvoidanceReason::FlowHasLineBoxContainProperty: |
| stream << "line-box-contain value indicates variable line height"; |
| break; |
| case AvoidanceReason::FlowIsNotTopToBottom: |
| stream << "non top-to-bottom flow"; |
| break; |
| case AvoidanceReason::FlowHasNonNormalUnicodeBiDi: |
| stream << "non-normal Unicode bidi"; |
| break; |
| case AvoidanceReason::FlowHasRTLOrdering: |
| stream << "-webkit-rtl-ordering"; |
| break; |
| case AvoidanceReason::FlowHasLineAlignEdges: |
| stream << "-webkit-line-align edges"; |
| break; |
| case AvoidanceReason::FlowHasLineSnap: |
| stream << "-webkit-line-snap property"; |
| break; |
| case AvoidanceReason::FlowHasTextEmphasisFillOrMark: |
| stream << "text-emphasis (fill/mark)"; |
| break; |
| case AvoidanceReason::FlowHasPseudoFirstLine: |
| stream << "first-line"; |
| break; |
| case AvoidanceReason::FlowHasPseudoFirstLetter: |
| stream << "first-letter"; |
| break; |
| case AvoidanceReason::FlowHasTextCombine: |
| stream << "text combine"; |
| break; |
| case AvoidanceReason::FlowHasTextFillBox: |
| stream << "background-color (text-fill)"; |
| break; |
| case AvoidanceReason::FlowHasBorderFitLines: |
| stream << "-webkit-border-fit"; |
| break; |
| case AvoidanceReason::FlowHasNonAutoLineBreak: |
| stream << "line-break is not auto"; |
| break; |
| case AvoidanceReason::FlowHasTextSecurity: |
| stream << "text-security is not none"; |
| break; |
| case AvoidanceReason::FlowHasSVGFont: |
| stream << "SVG font"; |
| break; |
| case AvoidanceReason::FlowTextHasDirectionCharacter: |
| stream << "direction character"; |
| break; |
| case AvoidanceReason::FlowIsMissingPrimaryFont: |
| stream << "missing primary font"; |
| break; |
| case AvoidanceReason::FlowPrimaryFontIsInsufficient: |
| stream << "missing glyph or glyph needs another font"; |
| break; |
| case AvoidanceReason::FlowTextIsCombineText: |
| stream << "text is combine"; |
| break; |
| case AvoidanceReason::FlowTextIsRenderCounter: |
| stream << "RenderCounter"; |
| break; |
| case AvoidanceReason::FlowTextIsRenderQuote: |
| stream << "RenderQuote"; |
| break; |
| case AvoidanceReason::FlowTextIsTextFragment: |
| stream << "TextFragment"; |
| break; |
| case AvoidanceReason::FlowTextIsSVGInlineText: |
| stream << "SVGInlineText"; |
| break; |
| case AvoidanceReason::FlowHasComplexFontCodePath: |
| stream << "text with complex font codepath"; |
| break; |
| case AvoidanceReason::FlowHasTextShadow: |
| stream << "text-shadow"; |
| break; |
| case AvoidanceReason::FlowChildIsSelected: |
| stream << "selected content"; |
| break; |
| case AvoidanceReason::FlowFontHasOverflowGlyph: |
| stream << "-webkit-line-box-contain: glyphs with overflowing text."; |
| break; |
| case AvoidanceReason::FlowTextHasSurrogatePair: |
| stream << "surrogate pair"; |
| break; |
| case AvoidanceReason::MultiColumnFlowIsNotTopLevel: |
| stream << "non top level column"; |
| break; |
| case AvoidanceReason::MultiColumnFlowHasColumnSpanner: |
| stream << "column has spanner"; |
| break; |
| case AvoidanceReason::MultiColumnFlowVerticalAlign: |
| stream << "column with vertical-align != baseline"; |
| break; |
| case AvoidanceReason::MultiColumnFlowIsFloating: |
| stream << "column with floating objects"; |
| break; |
| case AvoidanceReason::FlowIncludesDocumentMarkers: |
| stream << "text includes document markers"; |
| break; |
| case AvoidanceReason::FlowIncludesHighlights: |
| stream << "text includes highlights"; |
| break; |
| case AvoidanceReason::FlowHasJustifiedNonBreakingSpace: |
| stream << "justified text has non-breaking-space character"; |
| break; |
| case AvoidanceReason::FlowDoesNotEstablishInlineFormattingContext: |
| stream << "flow does not establishes inline formatting context"; |
| break; |
| case AvoidanceReason::UnsupportedFieldset: |
| stream << "fieldset box"; |
| break; |
| case AvoidanceReason::ChildBoxIsFloatingOrPositioned: |
| stream << "child box is floating or positioned"; |
| break; |
| case AvoidanceReason::ContentIsSVG: |
| stream << "SVG content"; |
| break; |
| case AvoidanceReason::ChildBoxHasUnsupportedStyle: |
| stream << "child box has unsupported style"; |
| break; |
| case AvoidanceReason::UnsupportedImageMap: |
| stream << "image map"; |
| break; |
| case AvoidanceReason::InlineBoxNeedsLayer: |
| stream << "inline box needs layer"; |
| break; |
| case AvoidanceReason::InlineBoxHasBorderOrBorderImage: |
| stream << "inline box has border or border image"; |
| break; |
| case AvoidanceReason::InlineBoxHasBackground: |
| stream << "inline box has background"; |
| break; |
| case AvoidanceReason::InlineBoxHasMarginOrPadding: |
| stream << "inline box has margin or padding"; |
| break; |
| default: |
| break; |
| } |
| } |
| |
| static void printReasons(OptionSet<AvoidanceReason> reasons, TextStream& stream) |
| { |
| stream << " "; |
| for (auto reason : reasons) { |
| printReason(reason, stream); |
| stream << ", "; |
| } |
| } |
| |
| static void printTextForSubtree(const RenderObject& renderer, unsigned& charactersLeft, TextStream& stream) |
| { |
| if (!charactersLeft) |
| return; |
| if (is<RenderText>(renderer)) { |
| String text = downcast<RenderText>(renderer).text(); |
| text = text.stripWhiteSpace(); |
| unsigned len = std::min(charactersLeft, text.length()); |
| stream << text.left(len); |
| charactersLeft -= len; |
| return; |
| } |
| if (!is<RenderElement>(renderer)) |
| return; |
| for (const auto* child = downcast<RenderElement>(renderer).firstChild(); child; child = child->nextSibling()) |
| printTextForSubtree(*child, charactersLeft, stream); |
| } |
| |
| static unsigned textLengthForSubtree(const RenderObject& renderer) |
| { |
| if (is<RenderText>(renderer)) |
| return downcast<RenderText>(renderer).text().length(); |
| if (!is<RenderElement>(renderer)) |
| return 0; |
| unsigned textLength = 0; |
| for (const auto* child = downcast<RenderElement>(renderer).firstChild(); child; child = child->nextSibling()) |
| textLength += textLengthForSubtree(*child); |
| return textLength; |
| } |
| |
| static void collectNonEmptyLeafRenderBlockFlows(const RenderObject& renderer, HashSet<const RenderBlockFlow*>& leafRenderers) |
| { |
| if (is<RenderText>(renderer)) { |
| if (!downcast<RenderText>(renderer).text().length()) |
| return; |
| // Find RenderBlockFlow ancestor. |
| for (const auto* current = renderer.parent(); current; current = current->parent()) { |
| if (!is<RenderBlockFlow>(current)) |
| continue; |
| leafRenderers.add(downcast<RenderBlockFlow>(current)); |
| break; |
| } |
| return; |
| } |
| if (!is<RenderElement>(renderer)) |
| return; |
| for (const auto* child = downcast<RenderElement>(renderer).firstChild(); child; child = child->nextSibling()) |
| collectNonEmptyLeafRenderBlockFlows(*child, leafRenderers); |
| } |
| |
| static void collectNonEmptyLeafRenderBlockFlowsForCurrentPage(HashSet<const RenderBlockFlow*>& leafRenderers) |
| { |
| for (const auto* document : Document::allDocuments()) { |
| if (!document->renderView() || document->backForwardCacheState() != Document::NotInBackForwardCache) |
| continue; |
| if (!document->isHTMLDocument() && !document->isXHTMLDocument()) |
| continue; |
| collectNonEmptyLeafRenderBlockFlows(*document->renderView(), leafRenderers); |
| } |
| } |
| |
| static void printModernLineLayoutBlockList(void) |
| { |
| HashSet<const RenderBlockFlow*> leafRenderers; |
| collectNonEmptyLeafRenderBlockFlowsForCurrentPage(leafRenderers); |
| if (!leafRenderers.size()) { |
| WTFLogAlways("No text found in this document\n"); |
| return; |
| } |
| TextStream stream; |
| stream << "---------------------------------------------------\n"; |
| for (const auto* flow : leafRenderers) { |
| auto reasons = canUseForLineLayoutWithReason(*flow, IncludeReasons::All); |
| if (reasons.isEmpty()) |
| continue; |
| unsigned printedLength = 30; |
| stream << "\""; |
| printTextForSubtree(*flow, printedLength, stream); |
| for (;printedLength > 0; --printedLength) |
| stream << " "; |
| stream << "\"(" << textLengthForSubtree(*flow) << "):"; |
| printReasons(reasons, stream); |
| stream << "\n"; |
| } |
| stream << "---------------------------------------------------\n"; |
| WTFLogAlways("%s", stream.release().utf8().data()); |
| } |
| |
| static void printModernLineLayoutCoverage(void) |
| { |
| HashSet<const RenderBlockFlow*> leafRenderers; |
| collectNonEmptyLeafRenderBlockFlowsForCurrentPage(leafRenderers); |
| if (!leafRenderers.size()) { |
| WTFLogAlways("No text found in this document\n"); |
| return; |
| } |
| TextStream stream; |
| HashMap<AvoidanceReason, unsigned, DefaultHash<uint64_t>, WTF::UnsignedWithZeroKeyHashTraits<uint64_t>> flowStatistics; |
| unsigned textLength = 0; |
| unsigned unsupportedTextLength = 0; |
| unsigned numberOfUnsupportedLeafBlocks = 0; |
| unsigned supportedButForcedToLineLayoutTextLength = 0; |
| unsigned numberOfSupportedButForcedToLineLayoutLeafBlocks = 0; |
| for (const auto* flow : leafRenderers) { |
| auto flowLength = textLengthForSubtree(*flow); |
| textLength += flowLength; |
| auto reasons = canUseForLineLayoutWithReason(*flow, IncludeReasons::All); |
| if (reasons.isEmpty()) { |
| if (flow->lineLayoutPath() == RenderBlockFlow::ForceLineBoxesPath) { |
| supportedButForcedToLineLayoutTextLength += flowLength; |
| ++numberOfSupportedButForcedToLineLayoutLeafBlocks; |
| } |
| continue; |
| } |
| ++numberOfUnsupportedLeafBlocks; |
| unsupportedTextLength += flowLength; |
| for (auto reason : reasons) { |
| auto result = flowStatistics.add(static_cast<uint64_t>(reason), flowLength); |
| if (!result.isNewEntry) |
| result.iterator->value += flowLength; |
| } |
| } |
| stream << "---------------------------------------------------\n"; |
| if (supportedButForcedToLineLayoutTextLength) { |
| stream << "Modern line layout potential coverage: " << (float)(textLength - unsupportedTextLength) / (float)textLength * 100 << "%\n\n"; |
| stream << "Modern line layout actual coverage: " << (float)(textLength - unsupportedTextLength - supportedButForcedToLineLayoutTextLength) / (float)textLength * 100 << "%\nForced line layout blocks: " << numberOfSupportedButForcedToLineLayoutLeafBlocks << " content length: " << supportedButForcedToLineLayoutTextLength << "(" << (float)supportedButForcedToLineLayoutTextLength / (float)textLength * 100 << "%)"; |
| } else |
| stream << "Modern line layout coverage: " << (float)(textLength - unsupportedTextLength) / (float)textLength * 100 << "%"; |
| stream << "\n\n"; |
| stream << "Number of blocks: total(" << leafRenderers.size() << ") legacy(" << numberOfUnsupportedLeafBlocks << ")\nContent length: total(" << |
| textLength << ") legacy(" << unsupportedTextLength << ")\n"; |
| for (const auto& reasonEntry : flowStatistics) { |
| printReason(static_cast<AvoidanceReason>(reasonEntry.key), stream); |
| stream << ": " << (float)reasonEntry.value / (float)textLength * 100 << "%\n"; |
| } |
| stream << "---------------------------------------------------\n"; |
| WTFLogAlways("%s", stream.release().utf8().data()); |
| } |
| #endif |
| |
| template <typename CharacterType> OptionSet<AvoidanceReason> canUseForCharacter(CharacterType, bool textIsJustified, IncludeReasons); |
| |
| template<> OptionSet<AvoidanceReason> canUseForCharacter(UChar character, bool textIsJustified, IncludeReasons includeReasons) |
| { |
| OptionSet<AvoidanceReason> reasons; |
| if (textIsJustified) { |
| if (character == noBreakSpace) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasJustifiedNonBreakingSpace, reasons, includeReasons); |
| // 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<> OptionSet<AvoidanceReason> canUseForCharacter(LChar character, bool textIsJustified, IncludeReasons) |
| { |
| if (textIsJustified && character == noBreakSpace) |
| return { AvoidanceReason::FlowHasJustifiedNonBreakingSpace }; |
| return { }; |
| } |
| |
| template <typename CharacterType> |
| static OptionSet<AvoidanceReason> canUseForText(const CharacterType* text, unsigned length, const FontCascade& fontCascade, Optional<float> lineHeightConstraint, |
| bool textIsJustified, IncludeReasons includeReasons) |
| { |
| OptionSet<AvoidanceReason> 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) { |
| auto character = text[i]; |
| auto characterReasons = canUseForCharacter(character, textIsJustified, includeReasons); |
| if (characterReasons) |
| ADD_REASONS_AND_RETURN_IF_NEEDED(characterReasons, reasons, includeReasons); |
| |
| 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 reasons; |
| } |
| |
| static OptionSet<AvoidanceReason> canUseForText(StringView text, const FontCascade& fontCascade, Optional<float> lineHeightConstraint, bool textIsJustified, IncludeReasons includeReasons) |
| { |
| if (text.is8Bit()) |
| return canUseForText(text.characters8(), text.length(), fontCascade, lineHeightConstraint, textIsJustified, includeReasons); |
| return canUseForText(text.characters16(), text.length(), fontCascade, lineHeightConstraint, textIsJustified, includeReasons); |
| } |
| |
| static OptionSet<AvoidanceReason> canUseForFontAndText(const RenderBoxModelObject& container, IncludeReasons includeReasons) |
| { |
| OptionSet<AvoidanceReason> reasons; |
| // We assume that all lines have metrics based purely on the primary font. |
| const auto& style = container.style(); |
| auto& fontCascade = style.fontCascade(); |
| if (fontCascade.primaryFont().isInterstitial()) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowIsMissingPrimaryFont, reasons, includeReasons); |
| Optional<float> lineHeightConstraint; |
| if (style.lineBoxContain().contains(LineBoxContain::Glyphs)) |
| lineHeightConstraint = container.lineHeight(false, HorizontalLine, PositionOfInteriorLineBoxes).toFloat(); |
| bool flowIsJustified = style.textAlign() == TextAlignMode::Justify; |
| for (const auto& textRenderer : childrenOfType<RenderText>(container)) { |
| // FIXME: Do not return until after checking all children. |
| 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 { |
| WebCore::TextRun run(String(textRenderer.text())); |
| run.setCharacterScanForCodePath(false); |
| if (style.fontCascade().codePath(run) != FontCascade::CodePath::Simple) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasComplexFontCodePath, reasons, includeReasons); |
| } |
| |
| auto textReasons = canUseForText(textRenderer.stringView(), fontCascade, lineHeightConstraint, flowIsJustified, includeReasons); |
| if (textReasons) |
| ADD_REASONS_AND_RETURN_IF_NEEDED(textReasons, reasons, includeReasons); |
| } |
| return reasons; |
| } |
| |
| static OptionSet<AvoidanceReason> canUseForStyle(const RenderStyle& style, IncludeReasons includeReasons) |
| { |
| OptionSet<AvoidanceReason> reasons; |
| if ((style.overflowX() != Overflow::Visible && style.overflowX() != Overflow::Hidden) |
| || (style.overflowY() != Overflow::Visible && style.overflowY() != Overflow::Hidden)) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasOverflowNotVisible, reasons, includeReasons); |
| if (style.textOverflow() == TextOverflow::Ellipsis) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasTextOverflow, reasons, includeReasons); |
| if (!style.textDecorationsInEffect().isEmpty() && (style.textUnderlinePosition() != TextUnderlinePosition::Auto || !style.textUnderlineOffset().isAuto() || !style.textDecorationThickness().isAuto())) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasUnsupportedUnderlineDecoration, reasons, includeReasons); |
| if (!style.isLeftToRightDirection()) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowIsNotLTR, reasons, includeReasons); |
| if (!(style.lineBoxContain().contains(LineBoxContain::Block))) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasLineBoxContainProperty, reasons, includeReasons); |
| if (style.writingMode() != WritingMode::TopToBottom) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowIsNotTopToBottom, reasons, includeReasons); |
| if (style.unicodeBidi() != UBNormal) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasNonNormalUnicodeBiDi, reasons, includeReasons); |
| if (style.rtlOrdering() != Order::Logical) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasRTLOrdering, reasons, includeReasons); |
| if (style.lineAlign() != LineAlign::None) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasLineAlignEdges, reasons, includeReasons); |
| if (style.lineSnap() != LineSnap::None) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasLineSnap, reasons, includeReasons); |
| if (style.textEmphasisFill() != TextEmphasisFill::Filled || style.textEmphasisMark() != TextEmphasisMark::None) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasTextEmphasisFillOrMark, reasons, includeReasons); |
| if (style.textShadow()) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasTextShadow, reasons, includeReasons); |
| if (style.hasPseudoStyle(PseudoId::FirstLine)) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasPseudoFirstLine, reasons, includeReasons); |
| if (style.hasPseudoStyle(PseudoId::FirstLetter)) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasPseudoFirstLetter, reasons, includeReasons); |
| if (style.hasTextCombine()) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasTextCombine, reasons, includeReasons); |
| if (style.backgroundClip() == FillBox::Text) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasTextFillBox, reasons, includeReasons); |
| if (style.borderFit() == BorderFit::Lines) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasBorderFitLines, reasons, includeReasons); |
| if (style.lineBreak() != LineBreak::Auto) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasNonAutoLineBreak, reasons, includeReasons); |
| if (style.nbspMode() != NBSPMode::Normal) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasWebKitNBSPMode, reasons, includeReasons); |
| // Special handling of text-security:disc is not yet implemented in the simple line layout code path. |
| // See RenderBlock::updateSecurityDiscCharacters. |
| if (style.textSecurity() != TextSecurity::None) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasTextSecurity, reasons, includeReasons); |
| if (style.hyphens() == Hyphens::Auto) { |
| auto textReasons = canUseForText(style.hyphenString(), style.fontCascade(), WTF::nullopt, false, includeReasons); |
| if (textReasons) |
| ADD_REASONS_AND_RETURN_IF_NEEDED(textReasons, reasons, includeReasons); |
| } |
| return reasons; |
| } |
| |
| static OptionSet<AvoidanceReason> canUseForChild(const RenderObject& child, IncludeReasons includeReasons) |
| { |
| OptionSet<AvoidanceReason> reasons; |
| if (child.selectionState() != RenderObject::HighlightState::None) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowChildIsSelected, reasons, includeReasons); |
| if (is<RenderCounter>(child)) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowTextIsRenderCounter, reasons, includeReasons); |
| if (is<RenderText>(child)) { |
| const auto& renderText = downcast<RenderText>(child); |
| if (renderText.textNode() && !renderText.document().markers().markersFor(*renderText.textNode()).isEmpty()) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowIncludesDocumentMarkers, reasons, includeReasons); |
| return reasons; |
| } |
| |
| if (is<RenderLineBreak>(child)) |
| return reasons; |
| |
| if (child.isFieldset()) { |
| // Fieldsets don't follow the standard CSS box model. They require special handling. |
| SET_REASON_AND_RETURN_IF_NEEDED(UnsupportedFieldset, reasons, includeReasons) |
| } |
| |
| #if ALLOW_IMAGES || ALLOW_ALL_REPLACED || ALLOW_INLINE_BLOCK |
| auto isSupportedStyle = [] (const auto& style) { |
| if (style.verticalAlign() == VerticalAlign::Sub || style.verticalAlign() == VerticalAlign::Super) |
| return false; |
| if (style.width().isPercent() || style.height().isPercent()) |
| return false; |
| if (style.minWidth().isPercent() || style.maxWidth().isPercent()) |
| return false; |
| if (style.minHeight().isPercent() || style.maxHeight().isPercent()) |
| return false; |
| return true; |
| }; |
| #endif |
| #if ALLOW_IMAGES || ALLOW_ALL_REPLACED |
| if (is<RenderReplaced>(child)) { |
| auto& replaced = downcast<RenderReplaced>(child); |
| if (replaced.isFloating() || replaced.isPositioned()) |
| SET_REASON_AND_RETURN_IF_NEEDED(ChildBoxIsFloatingOrPositioned, reasons, includeReasons) |
| |
| if (replaced.isSVGRoot()) |
| SET_REASON_AND_RETURN_IF_NEEDED(ContentIsSVG, reasons, includeReasons); |
| |
| if (!isSupportedStyle(replaced.style())) |
| SET_REASON_AND_RETURN_IF_NEEDED(ChildBoxHasUnsupportedStyle, reasons, includeReasons); |
| |
| if (is<RenderImage>(replaced)) { |
| auto& image = downcast<RenderImage>(replaced); |
| if (image.imageMap()) |
| SET_REASON_AND_RETURN_IF_NEEDED(UnsupportedImageMap, reasons, includeReasons); |
| return reasons; |
| } |
| #if !ALLOW_ALL_REPLACED |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasNonSupportedChild, reasons, includeReasons); |
| #endif |
| return reasons; |
| } |
| #endif |
| |
| #if ALLOW_INLINE_BLOCK |
| if (is<RenderBlockFlow>(child)) { |
| auto& block = downcast<RenderBlockFlow>(child); |
| if (!block.isReplaced() || !block.isInline()) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasNonSupportedChild, reasons, includeReasons) |
| if (block.isFloating() || block.isPositioned()) |
| SET_REASON_AND_RETURN_IF_NEEDED(ChildBoxIsFloatingOrPositioned, reasons, includeReasons) |
| if (block.isRubyRun()) |
| SET_REASON_AND_RETURN_IF_NEEDED(ContentIsRuby, reasons, includeReasons); |
| |
| auto& style = block.style(); |
| if (!isSupportedStyle(style)) |
| SET_REASON_AND_RETURN_IF_NEEDED(ChildBoxHasUnsupportedStyle, reasons, includeReasons) |
| if (style.display() != DisplayType::InlineBlock) |
| SET_REASON_AND_RETURN_IF_NEEDED(ChildBoxHasUnsupportedStyle, reasons, includeReasons) |
| |
| return reasons; |
| } |
| #endif |
| |
| #if ALLOW_INLINES |
| if (is<RenderInline>(child)) { |
| auto& renderInline = downcast<RenderInline>(child); |
| if (renderInline.isSVGInline()) |
| SET_REASON_AND_RETURN_IF_NEEDED(ContentIsSVG, reasons, includeReasons); |
| if (renderInline.isRubyInline() || renderInline.isQuote()) |
| SET_REASON_AND_RETURN_IF_NEEDED(ContentIsRuby, reasons, includeReasons); |
| if (renderInline.requiresLayer()) |
| SET_REASON_AND_RETURN_IF_NEEDED(InlineBoxNeedsLayer, reasons, includeReasons) |
| |
| auto& style = renderInline.style(); |
| if (!isSupportedStyle(style)) |
| SET_REASON_AND_RETURN_IF_NEEDED(ChildBoxHasUnsupportedStyle, reasons, includeReasons) |
| if (style.hasBorder() || style.borderImage().hasImage()) |
| SET_REASON_AND_RETURN_IF_NEEDED(InlineBoxHasBorderOrBorderImage, reasons, includeReasons); |
| if (style.hasBackground()) |
| SET_REASON_AND_RETURN_IF_NEEDED(InlineBoxHasBackground, reasons, includeReasons); |
| if (style.hasOutline()) |
| SET_REASON_AND_RETURN_IF_NEEDED(ContentHasOutline, reasons, includeReasons); |
| if (renderInline.marginLeft() < 0 || renderInline.marginRight() < 0 || renderInline.marginTop() < 0 || renderInline.marginBottom() < 0 |
| || renderInline.paddingLeft() < 0 || renderInline.paddingRight() < 0 || renderInline.paddingTop() < 0 || renderInline.paddingBottom() < 0) |
| SET_REASON_AND_RETURN_IF_NEEDED(InlineBoxHasMarginOrPadding, reasons, includeReasons); |
| if (renderInline.isInFlowPositioned()) |
| SET_REASON_AND_RETURN_IF_NEEDED(ChildBoxIsFloatingOrPositioned, reasons, includeReasons); |
| if (renderInline.containingBlock()->style().lineBoxContain() != RenderStyle::initialLineBoxContain()) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasLineBoxContainProperty, reasons, includeReasons); |
| auto fontAndTextReasons = canUseForFontAndText(downcast<RenderInline>(child), includeReasons); |
| if (fontAndTextReasons) |
| ADD_REASONS_AND_RETURN_IF_NEEDED(fontAndTextReasons, reasons, includeReasons); |
| auto styleReasons = canUseForStyle(style, includeReasons); |
| if (styleReasons) |
| ADD_REASONS_AND_RETURN_IF_NEEDED(styleReasons, reasons, includeReasons); |
| |
| return reasons; |
| } |
| #endif |
| |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasNonSupportedChild, reasons, includeReasons); |
| return reasons; |
| } |
| |
| OptionSet<AvoidanceReason> canUseForLineLayoutWithReason(const RenderBlockFlow& flow, IncludeReasons includeReasons) |
| { |
| #ifndef NDEBUG |
| static std::once_flag onceFlag; |
| std::call_once(onceFlag, [] { |
| PAL::registerNotifyCallback("com.apple.WebKit.showModernLineLayoutCoverage", WTF::Function<void()> { printModernLineLayoutCoverage }); |
| PAL::registerNotifyCallback("com.apple.WebKit.showModernLineLayoutReasons", WTF::Function<void()> { printModernLineLayoutBlockList }); |
| }); |
| #endif |
| OptionSet<AvoidanceReason> reasons; |
| // FIXME: For tests that disable SLL and expect to get CLL. |
| if (!flow.settings().simpleLineLayoutEnabled()) |
| SET_REASON_AND_RETURN_IF_NEEDED(FeatureIsDisabled, reasons, includeReasons); |
| auto establishesInlineFormattingContext = [&] { |
| if (flow.isRenderView()) { |
| // RenderView initiates a block formatting context. |
| return false; |
| } |
| ASSERT(flow.parent()); |
| // FIXME: This should really get the first _inflow_ child. |
| auto* firstChild = flow.firstChild(); |
| if (!firstChild) { |
| // Empty block containers do not initiate inline formatting context. |
| return false; |
| } |
| return firstChild->isInline() || firstChild->isInlineBlockOrInlineTable(); |
| }; |
| if (!establishesInlineFormattingContext()) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowDoesNotEstablishInlineFormattingContext, reasons, includeReasons); |
| if (flow.fragmentedFlowState() != RenderObject::NotInsideFragmentedFlow) { |
| auto* fragmentedFlow = flow.enclosingFragmentedFlow(); |
| if (!is<RenderMultiColumnFlow>(fragmentedFlow)) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowIsInsideANonMultiColumnThread, reasons, includeReasons); |
| auto& columnThread = downcast<RenderMultiColumnFlow>(*fragmentedFlow); |
| if (columnThread.parent() != &flow.view()) |
| SET_REASON_AND_RETURN_IF_NEEDED(MultiColumnFlowIsNotTopLevel, reasons, includeReasons); |
| if (columnThread.hasColumnSpanner()) |
| SET_REASON_AND_RETURN_IF_NEEDED(MultiColumnFlowHasColumnSpanner, reasons, includeReasons); |
| auto& style = flow.style(); |
| if (style.verticalAlign() != VerticalAlign::Baseline) |
| SET_REASON_AND_RETURN_IF_NEEDED(MultiColumnFlowVerticalAlign, reasons, includeReasons); |
| if (style.isFloating()) |
| SET_REASON_AND_RETURN_IF_NEEDED(MultiColumnFlowIsFloating, reasons, includeReasons); |
| } |
| if (!flow.isHorizontalWritingMode()) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasHorizonalWritingMode, reasons, includeReasons); |
| if (flow.hasOutline()) |
| SET_REASON_AND_RETURN_IF_NEEDED(ContentHasOutline, reasons, includeReasons); |
| if (flow.isRubyText() || flow.isRubyBase()) |
| SET_REASON_AND_RETURN_IF_NEEDED(ContentIsRuby, reasons, includeReasons); |
| if (!flow.style().hangingPunctuation().isEmpty()) |
| 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.document().highlightRegisterIfExists() && !flow.document().highlightRegisterIfExists()->map().isEmpty()) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowIncludesHighlights, reasons, includeReasons); |
| #if ENABLE(APP_HIGHLIGHTS) |
| if (flow.document().appHighlightRegisterIfExists() && !flow.document().appHighlightRegisterIfExists()->map().isEmpty()) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowIncludesHighlights, reasons, includeReasons); |
| #endif |
| if (flow.firstLineBlock()) |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasPseudoFirstLine, reasons, includeReasons); |
| if (flow.isAnonymousBlock() && flow.parent()->style().textOverflow() == TextOverflow::Ellipsis) |
| 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 (auto walker = InlineWalker(const_cast<RenderBlockFlow&>(flow)); !walker.atEnd(); walker.advance()) { |
| auto& child = *walker.current(); |
| if (!is<RenderText>(child) && flow.containsFloats()) { |
| // Non-text content may stretch the line and we don't yet have support for dynamic float avoiding (as the line grows). |
| SET_REASON_AND_RETURN_IF_NEEDED(FlowHasUnsupportedFloat, reasons, includeReasons); |
| } |
| auto childReasons = canUseForChild(child, includeReasons); |
| if (childReasons) |
| ADD_REASONS_AND_RETURN_IF_NEEDED(childReasons, reasons, includeReasons); |
| } |
| auto styleReasons = canUseForStyle(flow.style(), includeReasons); |
| if (styleReasons) |
| ADD_REASONS_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()) { |
| 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); |
| } |
| } |
| auto fontAndTextReasons = canUseForFontAndText(flow, includeReasons); |
| if (fontAndTextReasons) |
| ADD_REASONS_AND_RETURN_IF_NEEDED(fontAndTextReasons, reasons, includeReasons); |
| return reasons; |
| } |
| |
| bool canUseForLineLayout(const RenderBlockFlow& flow) |
| { |
| return canUseForLineLayoutWithReason(flow, IncludeReasons::First).isEmpty(); |
| } |
| |
| bool canUseForLineLayoutAfterStyleChange(const RenderBlockFlow& blockContainer, StyleDifference diff) |
| { |
| switch (diff) { |
| case StyleDifference::Equal: |
| case StyleDifference::RecompositeLayer: |
| return true; |
| case StyleDifference::Repaint: |
| case StyleDifference::RepaintIfTextOrBorderOrOutline: |
| case StyleDifference::RepaintLayer: |
| // FIXME: We could do a more focused style check by matching RendererStyle::changeRequiresRepaint&co. |
| return canUseForStyle(blockContainer.style(), IncludeReasons::First).isEmpty(); |
| case StyleDifference::LayoutPositionedMovementOnly: |
| return true; |
| case StyleDifference::SimplifiedLayout: |
| case StyleDifference::SimplifiedLayoutAndPositionedMovement: |
| return canUseForStyle(blockContainer.style(), IncludeReasons::First).isEmpty(); |
| case StyleDifference::Layout: |
| case StyleDifference::NewStyle: |
| return canUseForLineLayout(blockContainer); |
| } |
| ASSERT_NOT_REACHED(); |
| return canUseForLineLayout(blockContainer); |
| } |
| |
| } |
| } |
| |
| #endif |