Changeset 251455 in webkit


Ignore:
Timestamp:
Oct 22, 2019 2:11:48 PM (5 years ago)
Author:
Alan Bujtas
Message:

[LFC][IFC] Add support for continuous content/commit boundary check
https://bugs.webkit.org/show_bug.cgi?id=203255
<rdar://problem/56503598>

Reviewed by Antti Koivisto.

This patch adds support for continuous content and commit boundary check.

<span style="padding-right: 10px;">textcontent</span>

The content above forms a continuous, unbreakable run of ([container start][textcontent][container end with horizontal padding of 10px]).
However at this point we don't know yet whether we are at the end of the run and we need to keep checking for the trailing content.
In general, we can't submit the run to the line breaking unless we managed to find the commit boundary for the current run.

<span style="padding-right: 10px;">textcontent</span><img src="broken.jpg">

This content produces two separate runs as follows:

  1. ([container start][textcontent][container end with horizontal padding of 10px])
  2. ([img])

vs.
<span style="padding-right: 10px;">textcontent</span>moretextcontent

This content produces only one run

  1. ([container start][textcontent][container end with horizontal padding of 10px][moretextcontent])

The idea here is that we don't commit the content on the line unless we identified the run boundary. In practice it means that we hardly commit the current inline item on the line
but instead we use it to decide whether the uncommitted content is ready to be committed.

Using the following example:
<span style="padding-right: 10px;">textcontent<img src="broken.jpg"></span>

Incoming inline items are:

[container start] -> we can't identify the run boundary -> add inline item to pending content
[textcontent] -> we still can't identify the run boundary sine we don't know what the next inline item is -> add inline item to pending content
[img] -> now we know that the [container start][textcontent] is on a commit boundary -> commit pending content -> however the current [img] item's boundary is unknown.
[container end with horizontal padding of 10px] -> Now the [img] and [container end] form an unbreakable run but we don't yet know if this is a run boundary

  • End of content -> always a commit boundary -> commit pending items -> ([img][container end])
  • layout/inlineformatting/InlineLineBreaker.cpp:

(WebCore::Layout::LineBreaker::breakingContext):
(WebCore::Layout::LineBreaker::breakingContextForFloat):
(WebCore::Layout::LineBreaker::wordBreakingBehavior const):
(WebCore::Layout::LineBreaker::isAtBreakingOpportunity): Deleted.

  • layout/inlineformatting/InlineLineBreaker.h:
  • layout/inlineformatting/InlineLineLayout.cpp:

(WebCore::Layout::LineLayout::layout):
(WebCore::Layout::LineLayout::close):
(WebCore::Layout::LineLayout::placeInlineItem):
(WebCore::Layout::LineLayout::processUncommittedContent):
(WebCore::Layout::LineLayout::shouldProcessUncommittedContent const):

  • layout/inlineformatting/InlineLineLayout.h:

(WebCore::Layout::LineLayout::UncommittedContent::runs):
(WebCore::Layout::LineLayout::UncommittedContent::runs const):

Location:
trunk/Source/WebCore
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/Source/WebCore/ChangeLog

    r251454 r251455  
     12019-10-22  Zalan Bujtas  <zalan@apple.com>
     2
     3        [LFC][IFC] Add support for continuous content/commit boundary check
     4        https://bugs.webkit.org/show_bug.cgi?id=203255
     5        <rdar://problem/56503598>
     6
     7        Reviewed by Antti Koivisto.
     8
     9        This patch adds support for continuous content and commit boundary check.
     10       
     11        <span style="padding-right: 10px;">textcontent</span>
     12
     13        The content above forms a continuous, unbreakable run of ([container start][textcontent][container end with horizontal padding of 10px]).
     14        However at this point we don't know yet whether we are at the end of the run and we need to keep checking for the trailing content.
     15        In general, we can't submit the run to the line breaking unless we managed to find the commit boundary for the current run.
     16
     17        <span style="padding-right: 10px;">textcontent</span><img src="broken.jpg">
     18            This content produces two separate runs as follows:
     19            1. ([container start][textcontent][container end with horizontal padding of 10px])
     20            2. ([img])
     21        vs.
     22        <span style="padding-right: 10px;">textcontent</span>moretextcontent
     23            This content produces only one run
     24            1. ([container start][textcontent][container end with horizontal padding of 10px][moretextcontent])
     25
     26        The idea here is that we don't commit the content on the line unless we identified the run boundary. In practice it means that we hardly commit the current inline item on the line
     27        but instead we use it to decide whether the uncommitted content is ready to be committed.
     28
     29        Using the following example:       
     30        <span style="padding-right: 10px;">textcontent<img src="broken.jpg"></span>
     31       
     32        Incoming inline items are:
     33         [container start] -> we can't identify the run boundary -> add inline item to pending content
     34         [textcontent] -> we still can't identify the run boundary sine we don't know what the next inline item is -> add inline item to pending content
     35         [img] -> now we know that the [container start][textcontent] is on a commit boundary -> commit pending content -> however the current [img] item's boundary is unknown.
     36         [container end with horizontal padding of 10px] -> Now the [img] and [container end] form an unbreakable run but we don't yet know if this is a run boundary
     37         - End of content -> always a commit boundary -> commit pending items -> ([img][container end]) 
     38
     39        * layout/inlineformatting/InlineLineBreaker.cpp:
     40        (WebCore::Layout::LineBreaker::breakingContext):
     41        (WebCore::Layout::LineBreaker::breakingContextForFloat):
     42        (WebCore::Layout::LineBreaker::wordBreakingBehavior const):
     43        (WebCore::Layout::LineBreaker::isAtBreakingOpportunity): Deleted.
     44        * layout/inlineformatting/InlineLineBreaker.h:
     45        * layout/inlineformatting/InlineLineLayout.cpp:
     46        (WebCore::Layout::LineLayout::layout):
     47        (WebCore::Layout::LineLayout::close):
     48        (WebCore::Layout::LineLayout::placeInlineItem):
     49        (WebCore::Layout::LineLayout::processUncommittedContent):
     50        (WebCore::Layout::LineLayout::shouldProcessUncommittedContent const):
     51        * layout/inlineformatting/InlineLineLayout.h:
     52        (WebCore::Layout::LineLayout::UncommittedContent::runs):
     53        (WebCore::Layout::LineLayout::UncommittedContent::runs const):
     54
    1552019-10-22  Dean Jackson  <dino@apple.com>
    256
  • trunk/Source/WebCore/layout/inlineformatting/InlineLineBreaker.cpp

    r250766 r251455  
    3636namespace Layout {
    3737
    38 LineBreaker::BreakingContext LineBreaker::breakingContext(const InlineItem& inlineItem, LayoutUnit logicalWidth, const LineContext& lineContext)
     38LineBreaker::BreakingBehavior LineBreaker::breakingContext(const Vector<LineLayout::Run>& runs, LayoutUnit logicalWidth, LayoutUnit availableWidth, bool lineIsEmpty)
    3939{
    4040    // First content always stays on line.
    41     if (lineContext.isEmpty || logicalWidth <= lineContext.availableWidth)
    42         return { BreakingBehavior::Keep, isAtBreakingOpportunity(inlineItem) };
     41    if (lineIsEmpty || logicalWidth <= availableWidth)
     42        return BreakingBehavior::Keep;
    4343
    44     if (inlineItem.isHardLineBreak())
    45         return { BreakingBehavior::Keep, isAtBreakingOpportunity(inlineItem) };
     44    // FIXME: Look inside the list to find out whether it requires text content related line breaking (e.g <span>text</span> <- the first inline item here is a 'container start' but the content is text only).
     45    auto& inlineItem = runs.first().inlineItem;
     46    if (is<InlineTextItem>(inlineItem))
     47        return wordBreakingBehavior(runs, lineIsEmpty);
    4648
    47     if (is<InlineTextItem>(inlineItem))
    48         return { wordBreakingBehavior(downcast<InlineTextItem>(inlineItem), lineContext.isEmpty), isAtBreakingOpportunity(inlineItem) };
    49 
    50     // Wrap non-text boxes to the next line unless we can trim trailing whitespace.
    51     auto availableWidth = lineContext.availableWidth + lineContext.trimmableWidth;
    52     if (logicalWidth <= availableWidth)
    53         return { BreakingBehavior::Keep, isAtBreakingOpportunity(inlineItem) };
    54     return { BreakingBehavior::Wrap, isAtBreakingOpportunity(inlineItem) };
     49    return BreakingBehavior::Wrap;
    5550}
    5651
    57 LineBreaker::BreakingBehavior LineBreaker::wordBreakingBehavior(const InlineTextItem& inlineItem, bool lineIsEmpty) const
     52LineBreaker::BreakingBehavior LineBreaker::breakingContextForFloat(LayoutUnit floatLogicalWidth, LayoutUnit availableWidth, bool lineIsEmpty)
    5853{
     54    return (lineIsEmpty || floatLogicalWidth <= availableWidth) ? BreakingBehavior::Keep : BreakingBehavior::Wrap;
     55}
     56
     57LineBreaker::BreakingBehavior LineBreaker::wordBreakingBehavior(const Vector<LineLayout::Run>& runs, bool lineIsEmpty) const
     58{
     59    // FIXME: Check where the overflow occurs and use the corresponding style to figure out the breaking behaviour.
     60    // <span style="word-break: normal">first</span><span style="word-break: break-all">second</span><span style="word-break: normal">third</span>
     61    auto& inlineItem = downcast<InlineTextItem>(runs.first().inlineItem);
     62
    5963    // Word breaking behaviour:
    6064    // 1. Whitesapce collapse on -> push whitespace to next line.
     
    8791}
    8892
    89 bool LineBreaker::isAtBreakingOpportunity(const InlineItem& inlineItem)
    90 {
    91     if (inlineItem.isHardLineBreak())
    92         return true;
    93 
    94     if (is<InlineTextItem>(inlineItem))
    95         return downcast<InlineTextItem>(inlineItem).isWhitespace();
    96     return !inlineItem.isFloat() && !inlineItem.isContainerStart() && !inlineItem.isContainerEnd();
    97 }
    98 
    9993}
    10094}
  • trunk/Source/WebCore/layout/inlineformatting/InlineLineBreaker.h

    r246540 r251455  
    2828#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
    2929
     30#include "InlineLineLayout.h"
    3031#include "LayoutUnit.h"
    3132
     
    3940public:
    4041    enum class BreakingBehavior { Keep, Split, Wrap };
    41     struct BreakingContext {
    42         BreakingBehavior breakingBehavior;
    43         bool isAtBreakingOpportunity { false };
    44     };
    45     struct LineContext {
    46         LayoutUnit availableWidth;
    47         LayoutUnit logicalLeft;
    48         LayoutUnit trimmableWidth;
    49         bool isEmpty { false };
    50     };
    51     BreakingContext breakingContext(const InlineItem&, LayoutUnit logicalWidth, const LineContext&);
     42    BreakingBehavior breakingContext(const Vector<LineLayout::Run>&, LayoutUnit logicalWidth, LayoutUnit availableWidth, bool lineIsEmpty);
     43    BreakingBehavior breakingContextForFloat(LayoutUnit floatLogicalWidth, LayoutUnit availableWidth, bool lineIsEmpty);
    5244
    5345private:
    5446
    55     BreakingBehavior wordBreakingBehavior(const InlineTextItem&, bool lineIsEmpty) const;
    56     bool isAtBreakingOpportunity(const InlineItem&);
    57 
     47    BreakingBehavior wordBreakingBehavior(const Vector<LineLayout::Run>&, bool lineIsEmpty) const;
    5848    bool m_hyphenationIsDisabled { true };
    5949};
  • trunk/Source/WebCore/layout/inlineformatting/InlineLineLayout.cpp

    r251329 r251455  
    2929#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
    3030
     31#include "InlineLineBreaker.h"
    3132#include "LayoutBox.h"
    3233#include "TextUtil.h"
     
    122123
    123124    for (auto inlineItemIndex = firstNonPartialIndex; inlineItemIndex < m_lineInput.inlineItems.size(); ++inlineItemIndex) {
     125        // FIXME: We should not need to re-measure the dropped, uncommitted content when re-using them on the next line.
    124126        if (placeInlineItem(*m_lineInput.inlineItems[inlineItemIndex]) == IsEndOfLine::Yes)
    125127            return close();
    126128    }
    127     commitPendingContent();
     129    // Check the uncommitted content whether they fit now that we know we are at a commit boundary.
     130    if (!m_uncommittedContent.isEmpty())
     131        processUncommittedContent();
    128132    return close();
    129133}
     
    142146{
    143147    ASSERT(m_committedInlineItemCount || m_lineHasIntrusiveFloat);
     148    m_uncommittedContent.reset();
    144149    if (!m_committedInlineItemCount)
    145150        return LineContent { WTF::nullopt, WTFMove(m_floats), m_line.close(), m_line.lineBox() };
     
    156161LineLayout::IsEndOfLine LineLayout::placeInlineItem(const InlineItem& inlineItem)
    157162{
    158     auto availableWidth = m_line.availableWidth() - m_uncommittedContent.width();
    159     auto currentLogicalRight = m_line.lineBox().logicalRight() + m_uncommittedContent.width();
     163    auto currentLogicalRight = m_line.lineBox().logicalRight();
    160164    auto itemLogicalWidth = inlineItemWidth(formattingContext(), inlineItem, currentLogicalRight);
    161 
    162     // FIXME: Ensure LineContext::trimmableWidth includes uncommitted content if needed.
    163165    auto lineIsConsideredEmpty = !m_line.hasContent() && !m_lineHasIntrusiveFloat;
    164     auto breakingContext = m_lineBreaker.breakingContext(inlineItem, itemLogicalWidth, { availableWidth, currentLogicalRight, m_line.trailingTrimmableWidth(), lineIsConsideredEmpty });
    165     if (breakingContext.isAtBreakingOpportunity)
    166         commitPendingContent();
    167 
    168     // Content does not fit the current line.
    169     if (breakingContext.breakingBehavior == LineBreaker::BreakingBehavior::Wrap)
    170         return IsEndOfLine::Yes;
    171 
    172     // Partial content stays on the current line.
    173     if (breakingContext.breakingBehavior == LineBreaker::BreakingBehavior::Split) {
    174         ASSERT(inlineItem.isText());
    175         auto& inlineTextItem = downcast<InlineTextItem>(inlineItem);
    176         auto splitData = TextUtil::split(inlineTextItem.layoutBox(), inlineTextItem.start(), inlineTextItem.length(), itemLogicalWidth, availableWidth, currentLogicalRight);
    177         // Construct a partial trailing inline item.
    178         ASSERT(!m_trailingPartialInlineTextItem);
    179         m_trailingPartialInlineTextItem = inlineTextItem.split(splitData.start, splitData.length);
    180         m_uncommittedContent.add(*m_trailingPartialInlineTextItem, splitData.logicalWidth);
    181         commitPendingContent();
    182         return IsEndOfLine::Yes;
    183     }
    184 
    185     ASSERT(breakingContext.breakingBehavior == LineBreaker::BreakingBehavior::Keep);
     166
     167    // Floats are special, they are intrusive but they don't really participate in the line layout context.
    186168    if (inlineItem.isFloat()) {
     169        // FIXME: It gets a bit more complicated when there's some uncommitted content whether they should be added to the current line
     170        // e.g. text_content<div style="float: left"></div>continuous_text_content
     171        // Not sure what to do when the float takes up the available space and we've got continuous content. Browser engines don't agree.
     172        // Let's just commit the pending content and try placing the float for now.
     173        if (!m_uncommittedContent.isEmpty()) {
     174            if (processUncommittedContent() == IsEndOfLine::Yes)
     175                return IsEndOfLine::Yes;
     176        }
     177        auto breakingBehavior = LineBreaker().breakingContextForFloat(itemLogicalWidth, m_line.availableWidth() + m_line.trailingTrimmableWidth(), lineIsConsideredEmpty);
     178        ASSERT(breakingBehavior != LineBreaker::BreakingBehavior::Split);
     179        if (breakingBehavior == LineBreaker::BreakingBehavior::Wrap)
     180            return IsEndOfLine::Yes;
     181
     182        // This float can sit on the current line.
    187183        auto& floatBox = inlineItem.layoutBox();
    188184        // Shrink available space for current line and move existing inline runs.
    189         auto floatBoxWidth = formattingContext().geometryForBox(floatBox).marginBoxWidth();
    190         floatBox.isLeftFloatingPositioned() ? m_line.moveLogicalLeft(floatBoxWidth) : m_line.moveLogicalRight(floatBoxWidth);
     185        floatBox.isLeftFloatingPositioned() ? m_line.moveLogicalLeft(itemLogicalWidth) : m_line.moveLogicalRight(itemLogicalWidth);
    191186        m_floats.append(makeWeakPtr(inlineItem));
    192187        ++m_committedInlineItemCount;
     
    194189        return IsEndOfLine::No;
    195190    }
    196 
    197     m_uncommittedContent.add(inlineItem, itemLogicalWidth);
    198     if (breakingContext.isAtBreakingOpportunity)
     191    // Explicit line breaks are also special.
     192    if (inlineItem.isHardLineBreak()) {
     193        auto isEndOfLine = !m_uncommittedContent.isEmpty() ? processUncommittedContent() : IsEndOfLine::No;
     194        // When the uncommitted content fits(or the line is empty), add the line break to this line as well.
     195        if (isEndOfLine == IsEndOfLine::No) {
     196            m_uncommittedContent.add(inlineItem, itemLogicalWidth);
     197            commitPendingContent();
     198        }
     199        return IsEndOfLine::Yes;
     200    }
     201    //
     202    auto isEndOfLine = IsEndOfLine::No;
     203    if (!m_uncommittedContent.isEmpty() && shouldProcessUncommittedContent(inlineItem))
     204        isEndOfLine = processUncommittedContent();
     205    // The current item might fit as well.
     206    if (isEndOfLine == IsEndOfLine::No)
     207        m_uncommittedContent.add(inlineItem, itemLogicalWidth);
     208    return isEndOfLine;
     209}
     210
     211LineLayout::IsEndOfLine LineLayout::processUncommittedContent()
     212{
     213    // Check if the pending content fits.
     214    auto lineIsConsideredEmpty = !m_line.hasContent() && !m_lineHasIntrusiveFloat;
     215    auto breakingBehavior = LineBreaker().breakingContext(m_uncommittedContent.runs(), m_uncommittedContent.width(), m_line.availableWidth(), lineIsConsideredEmpty);
     216
     217    // The uncommitted content can fully, partially fit the current line (commit/partial commit) or not at all (reset).
     218    if (breakingBehavior == LineBreaker::BreakingBehavior::Keep)
    199219        commitPendingContent();
    200 
    201     return inlineItem.isHardLineBreak() ? IsEndOfLine::Yes : IsEndOfLine::No;
    202 }
     220    else if (breakingBehavior == LineBreaker::BreakingBehavior::Split)
     221        ASSERT_NOT_IMPLEMENTED_YET();
     222    else if (breakingBehavior == LineBreaker::BreakingBehavior::Wrap)
     223        m_uncommittedContent.reset();
     224
     225    return breakingBehavior == LineBreaker::BreakingBehavior::Keep ? IsEndOfLine::No :IsEndOfLine::Yes;
     226}
     227
     228bool LineLayout::shouldProcessUncommittedContent(const InlineItem& inlineItem) const
     229{
     230    // Figure out if the new incoming content puts the uncommitted content on commit boundary.
     231    // e.g. <span>continuous</span> <- uncomitted content ->
     232    // [inline container start][text content][inline container end]
     233    // An incoming <img> box would enable us to commit the "<span>continuous</span>" content
     234    // while additional text content would not.
     235    ASSERT(!inlineItem.isFloat() && !inlineItem.isHardLineBreak());
     236    ASSERT(!m_uncommittedContent.isEmpty());
     237
     238    auto* lastUncomittedContent = &m_uncommittedContent.runs().last().inlineItem;
     239    if (inlineItem.isText()) {
     240        // any content' ' -> whitespace is always a commit boundary.
     241        if (downcast<InlineTextItem>(inlineItem).isWhitespace())
     242            return true;
     243        // <span>text -> the inline container start and the text content form an unbreakable continuous content.
     244        if (lastUncomittedContent->isContainerStart())
     245            return false;
     246        // </span>text -> need to check what's before the </span>.
     247        // text</span>text -> continuous content
     248        // <img></span>text -> commit bounday
     249        if (lastUncomittedContent->isContainerEnd()) {
     250            auto& runs = m_uncommittedContent.runs();
     251            // text</span><span></span></span>text -> check all the way back until we hit either a box or some text
     252            for (auto i = m_uncommittedContent.size(); i--;) {
     253                auto& previousInlineItem = runs[i].inlineItem;
     254                if (previousInlineItem.isContainerStart() || previousInlineItem.isContainerEnd())
     255                    continue;
     256                ASSERT(previousInlineItem.isText() || previousInlineItem.isBox());
     257                lastUncomittedContent = &previousInlineItem;
     258                break;
     259            }
     260            // Did not find any content (e.g. <span></span>text)
     261            if (lastUncomittedContent->isContainerEnd())
     262                return false;
     263        }
     264        // texttext -> continuous content.
     265        // ' 'text -> commit boundary.
     266        if (lastUncomittedContent->isText())
     267            return downcast<InlineTextItem>(*lastUncomittedContent).isWhitespace();
     268        // <img>text -> the inline box is on a commit boundary.
     269        if (lastUncomittedContent->isBox())
     270            return true;
     271        ASSERT_NOT_REACHED();
     272    }
     273
     274    if (inlineItem.isBox()) {
     275        // <span><img> -> the inline container start and the content form an unbreakable continuous content.
     276        if (lastUncomittedContent->isContainerStart())
     277            return false;
     278        // </span><img> -> ok to commit the </span>.
     279        if (lastUncomittedContent->isContainerEnd())
     280            return true;
     281        // <img>text and <img><img> -> these combinations are ok to commit.
     282        if (lastUncomittedContent->isText() || lastUncomittedContent->isBox())
     283            return true;
     284        ASSERT_NOT_REACHED();
     285    }
     286
     287    if (inlineItem.isContainerStart() || inlineItem.isContainerEnd()) {
     288        // <span><span> or </span><span> -> can't commit the previous content yet.
     289        if (lastUncomittedContent->isContainerStart() || lastUncomittedContent->isContainerEnd())
     290            return false;
     291        // ' '<span> -> let's commit the whitespace
     292        // text<span> -> but not yet the non-whitespace; we need to know what comes next (e.g. text<span>text or text<span><img>).
     293        if (lastUncomittedContent->isText())
     294            return downcast<InlineTextItem>(*lastUncomittedContent).isWhitespace();
     295        // <img><span> -> it's ok to commit the inline box content.
     296        // <img></span> -> the inline box and the closing inline container form an unbreakable continuous content.
     297        if (lastUncomittedContent->isBox())
     298            return inlineItem.isContainerStart();
     299        ASSERT_NOT_REACHED();
     300    }
     301
     302    ASSERT_NOT_REACHED();
     303    return true;
     304}
     305
    203306
    204307}
  • trunk/Source/WebCore/layout/inlineformatting/InlineLineLayout.h

    r250490 r251455  
    2929
    3030#include "InlineLine.h"
    31 #include "InlineLineBreaker.h"
    3231#include <wtf/IsoMalloc.h>
    3332
     
    7271    };
    7372
     73    struct Run {
     74        const InlineItem& inlineItem;
     75        LayoutUnit logicalWidth;
     76    };
     77
    7478private:
    7579    const InlineFormattingContext& formattingContext() const { return m_inlineFormattingContext; }
     
    7882    void commitPendingContent();
    7983    LineContent close();
     84    bool shouldProcessUncommittedContent(const InlineItem&) const;
     85    IsEndOfLine processUncommittedContent();
    8086   
    8187    struct UncommittedContent {
    82         struct Run {
    83             const InlineItem& inlineItem;
    84             LayoutUnit logicalWidth;
    85         };
    8688        void add(const InlineItem&, LayoutUnit logicalWidth);
    8789        void reset();
    8890
    89         Vector<Run> runs() { return m_uncommittedRuns; }
     91        Vector<Run>& runs() { return m_uncommittedRuns; }
     92        const Vector<Run>& runs() const { return m_uncommittedRuns; }
    9093        bool isEmpty() const { return m_uncommittedRuns.isEmpty(); }
    9194        unsigned size() const { return m_uncommittedRuns.size(); }
     
    100103    const LineInput& m_lineInput;
    101104    Line m_line;
    102     LineBreaker m_lineBreaker;
    103105    bool m_lineHasIntrusiveFloat { false };
    104106    UncommittedContent m_uncommittedContent;
Note: See TracChangeset for help on using the changeset viewer.