| /* |
| * Copyright (C) 2019 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 "InlineLineLayout.h" |
| |
| #if ENABLE(LAYOUT_FORMATTING_CONTEXT) |
| |
| #include "InlineLineBreaker.h" |
| #include "LayoutBox.h" |
| #include "TextUtil.h" |
| |
| namespace WebCore { |
| namespace Layout { |
| |
| static LayoutUnit inlineItemWidth(const FormattingContext& formattingContext, const InlineItem& inlineItem, LayoutUnit contentLogicalLeft) |
| { |
| if (inlineItem.isLineBreak()) |
| return 0; |
| |
| if (is<InlineTextItem>(inlineItem)) { |
| auto& inlineTextItem = downcast<InlineTextItem>(inlineItem); |
| auto end = inlineTextItem.isCollapsible() ? inlineTextItem.start() + 1 : inlineTextItem.end(); |
| return TextUtil::width(inlineTextItem.layoutBox(), inlineTextItem.start(), end, contentLogicalLeft); |
| } |
| |
| auto& layoutBox = inlineItem.layoutBox(); |
| auto& boxGeometry = formattingContext.geometryForBox(layoutBox); |
| |
| if (layoutBox.isFloatingPositioned()) |
| return boxGeometry.marginBoxWidth(); |
| |
| if (layoutBox.replaced()) |
| return boxGeometry.width(); |
| |
| if (inlineItem.isContainerStart()) |
| return boxGeometry.marginStart() + boxGeometry.borderLeft() + boxGeometry.paddingLeft().valueOr(0); |
| |
| if (inlineItem.isContainerEnd()) |
| return boxGeometry.marginEnd() + boxGeometry.borderRight() + boxGeometry.paddingRight().valueOr(0); |
| |
| // Non-replaced inline box (e.g. inline-block) |
| return boxGeometry.width(); |
| } |
| |
| LineLayout::LineInput::LineInput(const Line::InitialConstraints& initialLineConstraints, TextAlignMode horizontalAlignment, IndexAndRange firstToProcess, const InlineItems& inlineItems) |
| : initialConstraints(initialLineConstraints) |
| , horizontalAlignment(horizontalAlignment) |
| , firstInlineItem(firstToProcess) |
| , inlineItems(inlineItems) |
| { |
| } |
| |
| LineLayout::LineInput::LineInput(const Line::InitialConstraints& initialLineConstraints, IndexAndRange firstToProcess, const InlineItems& inlineItems) |
| : initialConstraints(initialLineConstraints) |
| , skipAlignment(Line::SkipAlignment::Yes) |
| , firstInlineItem(firstToProcess) |
| , inlineItems(inlineItems) |
| { |
| } |
| |
| void LineLayout::UncommittedContent::add(const InlineItem& inlineItem, LayoutUnit logicalWidth) |
| { |
| m_uncommittedRuns.append({ inlineItem, logicalWidth }); |
| m_width += logicalWidth; |
| } |
| |
| void LineLayout::UncommittedContent::reset() |
| { |
| m_uncommittedRuns.clear(); |
| m_width = 0; |
| } |
| |
| LineLayout::LineLayout(const InlineFormattingContext& inlineFormattingContext, const LineInput& lineInput) |
| : m_inlineFormattingContext(inlineFormattingContext) |
| , m_lineInput(lineInput) |
| , m_line(inlineFormattingContext, lineInput.initialConstraints, lineInput.horizontalAlignment, lineInput.skipAlignment) |
| , m_lineHasIntrusiveFloat(lineInput.initialConstraints.lineIsConstrainedByFloat) |
| { |
| } |
| |
| LineLayout::LineContent LineLayout::layout() |
| { |
| // Iterate through the inline content and place the inline boxes on the current line. |
| // Start with the partial text from the previous line. |
| auto firstInlineItem = m_lineInput.firstInlineItem; |
| unsigned firstNonPartialIndex = firstInlineItem.index; |
| if (firstInlineItem.partialContext) { |
| // Handle partial inline item (split text from the previous line). |
| auto& originalTextItem = m_lineInput.inlineItems[firstInlineItem.index]; |
| RELEASE_ASSERT(originalTextItem->isText()); |
| |
| auto textRange = *firstInlineItem.partialContext; |
| // Construct a partial leading inline item. |
| ASSERT(!m_leadingPartialInlineTextItem); |
| m_leadingPartialInlineTextItem = downcast<InlineTextItem>(*originalTextItem).split(textRange.start, textRange.length); |
| if (placeInlineItem(*m_leadingPartialInlineTextItem) == IsEndOfLine::Yes) |
| return close(); |
| ++firstNonPartialIndex; |
| } |
| |
| for (auto inlineItemIndex = firstNonPartialIndex; inlineItemIndex < m_lineInput.inlineItems.size(); ++inlineItemIndex) { |
| // FIXME: We should not need to re-measure the dropped, uncommitted content when re-using them on the next line. |
| if (placeInlineItem(*m_lineInput.inlineItems[inlineItemIndex]) == IsEndOfLine::Yes) |
| return close(); |
| } |
| // Check the uncommitted content whether they fit now that we know we are at a commit boundary. |
| if (!m_uncommittedContent.isEmpty()) |
| processUncommittedContent(); |
| return close(); |
| } |
| |
| void LineLayout::commitPendingContent() |
| { |
| if (m_uncommittedContent.isEmpty()) |
| return; |
| m_committedInlineItemCount += m_uncommittedContent.size(); |
| for (auto& uncommittedRun : m_uncommittedContent.runs()) |
| m_line.append(uncommittedRun.inlineItem, uncommittedRun.logicalWidth); |
| m_uncommittedContent.reset(); |
| } |
| |
| LineLayout::LineContent LineLayout::close() |
| { |
| ASSERT(m_committedInlineItemCount || m_lineHasIntrusiveFloat); |
| m_uncommittedContent.reset(); |
| if (!m_committedInlineItemCount) |
| return LineContent { WTF::nullopt, WTFMove(m_floats), m_line.close(), m_line.lineBox() }; |
| |
| auto lastInlineItemIndex = m_lineInput.firstInlineItem.index + m_committedInlineItemCount - 1; |
| Optional<IndexAndRange::Range> partialContext; |
| if (m_trailingPartialInlineTextItem) |
| partialContext = IndexAndRange::Range { m_trailingPartialInlineTextItem->start(), m_trailingPartialInlineTextItem->length() }; |
| |
| auto lastCommitedItem = IndexAndRange { lastInlineItemIndex, partialContext }; |
| return LineContent { lastCommitedItem, WTFMove(m_floats), m_line.close(), m_line.lineBox() }; |
| } |
| |
| LineLayout::IsEndOfLine LineLayout::placeInlineItem(const InlineItem& inlineItem) |
| { |
| auto currentLogicalRight = m_line.lineBox().logicalRight(); |
| auto itemLogicalWidth = inlineItemWidth(formattingContext(), inlineItem, currentLogicalRight); |
| auto lineIsConsideredEmpty = !m_line.hasContent() && !m_lineHasIntrusiveFloat; |
| |
| // Floats are special, they are intrusive but they don't really participate in the line layout context. |
| if (inlineItem.isFloat()) { |
| // FIXME: It gets a bit more complicated when there's some uncommitted content whether they should be added to the current line |
| // e.g. text_content<div style="float: left"></div>continuous_text_content |
| // Not sure what to do when the float takes up the available space and we've got continuous content. Browser engines don't agree. |
| // Let's just commit the pending content and try placing the float for now. |
| if (!m_uncommittedContent.isEmpty()) { |
| if (processUncommittedContent() == IsEndOfLine::Yes) |
| return IsEndOfLine::Yes; |
| } |
| auto breakingBehavior = LineBreaker().breakingContextForFloat(itemLogicalWidth, m_line.availableWidth() + m_line.trailingTrimmableWidth(), lineIsConsideredEmpty); |
| ASSERT(breakingBehavior != LineBreaker::BreakingBehavior::Split); |
| if (breakingBehavior == LineBreaker::BreakingBehavior::Wrap) |
| return IsEndOfLine::Yes; |
| |
| // This float can sit on the current line. |
| auto& floatBox = inlineItem.layoutBox(); |
| // Shrink available space for current line and move existing inline runs. |
| floatBox.isLeftFloatingPositioned() ? m_line.moveLogicalLeft(itemLogicalWidth) : m_line.moveLogicalRight(itemLogicalWidth); |
| m_floats.append(makeWeakPtr(inlineItem)); |
| ++m_committedInlineItemCount; |
| m_lineHasIntrusiveFloat = true; |
| return IsEndOfLine::No; |
| } |
| // Explicit line breaks are also special. |
| if (inlineItem.isHardLineBreak()) { |
| auto isEndOfLine = !m_uncommittedContent.isEmpty() ? processUncommittedContent() : IsEndOfLine::No; |
| // When the uncommitted content fits(or the line is empty), add the line break to this line as well. |
| if (isEndOfLine == IsEndOfLine::No) { |
| m_uncommittedContent.add(inlineItem, itemLogicalWidth); |
| commitPendingContent(); |
| } |
| return IsEndOfLine::Yes; |
| } |
| // |
| auto isEndOfLine = IsEndOfLine::No; |
| if (!m_uncommittedContent.isEmpty() && shouldProcessUncommittedContent(inlineItem)) |
| isEndOfLine = processUncommittedContent(); |
| // The current item might fit as well. |
| if (isEndOfLine == IsEndOfLine::No) |
| m_uncommittedContent.add(inlineItem, itemLogicalWidth); |
| return isEndOfLine; |
| } |
| |
| LineLayout::IsEndOfLine LineLayout::processUncommittedContent() |
| { |
| // Check if the pending content fits. |
| auto lineIsConsideredEmpty = !m_line.hasContent() && !m_lineHasIntrusiveFloat; |
| auto breakingBehavior = LineBreaker().breakingContext(m_uncommittedContent.runs(), m_uncommittedContent.width(), m_line.availableWidth(), lineIsConsideredEmpty); |
| |
| // The uncommitted content can fully, partially fit the current line (commit/partial commit) or not at all (reset). |
| if (breakingBehavior == LineBreaker::BreakingBehavior::Keep) |
| commitPendingContent(); |
| else if (breakingBehavior == LineBreaker::BreakingBehavior::Split) |
| ASSERT_NOT_IMPLEMENTED_YET(); |
| else if (breakingBehavior == LineBreaker::BreakingBehavior::Wrap) |
| m_uncommittedContent.reset(); |
| |
| return breakingBehavior == LineBreaker::BreakingBehavior::Keep ? IsEndOfLine::No :IsEndOfLine::Yes; |
| } |
| |
| bool LineLayout::shouldProcessUncommittedContent(const InlineItem& inlineItem) const |
| { |
| // Figure out if the new incoming content puts the uncommitted content on commit boundary. |
| // e.g. <span>continuous</span> <- uncomitted content -> |
| // [inline container start][text content][inline container end] |
| // An incoming <img> box would enable us to commit the "<span>continuous</span>" content |
| // while additional text content would not. |
| ASSERT(!inlineItem.isFloat() && !inlineItem.isHardLineBreak()); |
| ASSERT(!m_uncommittedContent.isEmpty()); |
| |
| auto* lastUncomittedContent = &m_uncommittedContent.runs().last().inlineItem; |
| if (inlineItem.isText()) { |
| // any content' ' -> whitespace is always a commit boundary. |
| if (downcast<InlineTextItem>(inlineItem).isWhitespace()) |
| return true; |
| // <span>text -> the inline container start and the text content form an unbreakable continuous content. |
| if (lastUncomittedContent->isContainerStart()) |
| return false; |
| // </span>text -> need to check what's before the </span>. |
| // text</span>text -> continuous content |
| // <img></span>text -> commit bounday |
| if (lastUncomittedContent->isContainerEnd()) { |
| auto& runs = m_uncommittedContent.runs(); |
| // text</span><span></span></span>text -> check all the way back until we hit either a box or some text |
| for (auto i = m_uncommittedContent.size(); i--;) { |
| auto& previousInlineItem = runs[i].inlineItem; |
| if (previousInlineItem.isContainerStart() || previousInlineItem.isContainerEnd()) |
| continue; |
| ASSERT(previousInlineItem.isText() || previousInlineItem.isBox()); |
| lastUncomittedContent = &previousInlineItem; |
| break; |
| } |
| // Did not find any content (e.g. <span></span>text) |
| if (lastUncomittedContent->isContainerEnd()) |
| return false; |
| } |
| // texttext -> continuous content. |
| // ' 'text -> commit boundary. |
| if (lastUncomittedContent->isText()) |
| return downcast<InlineTextItem>(*lastUncomittedContent).isWhitespace(); |
| // <img>text -> the inline box is on a commit boundary. |
| if (lastUncomittedContent->isBox()) |
| return true; |
| ASSERT_NOT_REACHED(); |
| } |
| |
| if (inlineItem.isBox()) { |
| // <span><img> -> the inline container start and the content form an unbreakable continuous content. |
| if (lastUncomittedContent->isContainerStart()) |
| return false; |
| // </span><img> -> ok to commit the </span>. |
| if (lastUncomittedContent->isContainerEnd()) |
| return true; |
| // <img>text and <img><img> -> these combinations are ok to commit. |
| if (lastUncomittedContent->isText() || lastUncomittedContent->isBox()) |
| return true; |
| ASSERT_NOT_REACHED(); |
| } |
| |
| if (inlineItem.isContainerStart() || inlineItem.isContainerEnd()) { |
| // <span><span> or </span><span> -> can't commit the previous content yet. |
| if (lastUncomittedContent->isContainerStart() || lastUncomittedContent->isContainerEnd()) |
| return false; |
| // ' '<span> -> let's commit the whitespace |
| // text<span> -> but not yet the non-whitespace; we need to know what comes next (e.g. text<span>text or text<span><img>). |
| if (lastUncomittedContent->isText()) |
| return downcast<InlineTextItem>(*lastUncomittedContent).isWhitespace(); |
| // <img><span> -> it's ok to commit the inline box content. |
| // <img></span> -> the inline box and the closing inline container form an unbreakable continuous content. |
| if (lastUncomittedContent->isBox()) |
| return inlineItem.isContainerStart(); |
| ASSERT_NOT_REACHED(); |
| } |
| |
| ASSERT_NOT_REACHED(); |
| return true; |
| } |
| |
| |
| } |
| } |
| |
| #endif |