blob: ad89be9b5b3b490f7c671e2b368ca7c32aad55bb [file] [log] [blame]
/*
* 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 "InlineLine.h"
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
#include "InlineFormattingContext.h"
#include "TextUtil.h"
#include <wtf/IsoMallocInlines.h>
namespace WebCore {
namespace Layout {
WTF_MAKE_ISO_ALLOCATED_IMPL(Line);
Line::Run::Run(const InlineItem& inlineItem, const Display::Run& displayRun)
: m_inlineItem(inlineItem)
, m_displayRun(displayRun)
{
}
bool Line::Run::isWhitespace() const
{
if (!isText())
return false;
return downcast<InlineTextItem>(m_inlineItem).isWhitespace();
}
bool Line::Run::canBeExtended() const
{
if (!isText())
return false;
return !downcast<InlineTextItem>(m_inlineItem).isCollapsed() && !isVisuallyEmpty();
}
Line::Line(const InlineFormattingContext& inlineFormattingContext, const InitialConstraints& initialConstraints, Optional<TextAlignMode> horizontalAlignment, SkipAlignment skipAlignment)
: m_inlineFormattingContext(inlineFormattingContext)
, m_initialStrut(initialConstraints.heightAndBaseline ? initialConstraints.heightAndBaseline->strut : WTF::nullopt)
, m_lineLogicalWidth(initialConstraints.availableLogicalWidth)
, m_horizontalAlignment(horizontalAlignment)
, m_skipAlignment(skipAlignment == SkipAlignment::Yes)
{
ASSERT(m_skipAlignment || initialConstraints.heightAndBaseline);
auto initialLineHeight = initialConstraints.heightAndBaseline ? initialConstraints.heightAndBaseline->height : LayoutUnit();
auto initialBaselineOffset = initialConstraints.heightAndBaseline ? initialConstraints.heightAndBaseline->baselineOffset : LayoutUnit();
auto lineRect = Display::Rect { initialConstraints.logicalTopLeft, { }, initialLineHeight };
auto baseline = LineBox::Baseline { initialBaselineOffset, initialLineHeight - initialBaselineOffset };
m_lineBox = LineBox { lineRect, baseline, initialBaselineOffset };
}
static bool isInlineContainerConsideredEmpty(const FormattingContext& formattingContext, const Box& layoutBox)
{
// Note that this does not check whether the inline container has content. It simply checks if the container itself is considered empty.
auto& boxGeometry = formattingContext.geometryForBox(layoutBox);
return !(boxGeometry.horizontalBorder() || (boxGeometry.horizontalPadding() && boxGeometry.horizontalPadding().value()));
}
bool Line::isVisuallyEmpty() const
{
// FIXME: This should be cached instead -as the inline items are being added.
// Return true for empty inline containers like <span></span>.
auto& formattingContext = this->formattingContext();
for (auto& run : m_runList) {
if (run->isContainerStart()) {
if (!isInlineContainerConsideredEmpty(formattingContext, run->layoutBox()))
return false;
continue;
}
if (run->isContainerEnd())
continue;
if (run->layoutBox().establishesFormattingContext()) {
ASSERT(run->layoutBox().isInlineBlockBox());
auto& boxGeometry = formattingContext.geometryForBox(run->layoutBox());
if (!boxGeometry.width())
continue;
if (m_skipAlignment || boxGeometry.height())
return false;
continue;
}
if (!run->isText() || !run->isVisuallyEmpty())
return false;
}
return true;
}
Line::RunList Line::close()
{
removeTrailingTrimmableContent();
// Join text runs together when possible.
unsigned index = 1;
while (index < m_runList.size()) {
auto& previousRun = m_runList[index - 1];
if (!previousRun->canBeExtended()) {
++index;
continue;
}
auto& currentRun = m_runList[index];
if (&currentRun->layoutBox() != &previousRun->layoutBox()) {
// Do not merge runs from different boxes (<span>foo</span><span>bar</span>).
++index;
continue;
}
// Only text content can be extended atm.
ASSERT(previousRun->isText());
ASSERT(currentRun->isText());
previousRun->expand(*currentRun);
m_runList.remove(index);
}
if (!m_skipAlignment) {
alignContentVertically();
alignContentHorizontally();
}
return WTFMove(m_runList);
}
void Line::alignContentVertically()
{
ASSERT(!m_skipAlignment);
if (isVisuallyEmpty()) {
m_lineBox.resetBaseline();
m_lineBox.setLogicalHeight({ });
}
// Remove descent when all content is baseline aligned but none of them have descent.
if (formattingContext().quirks().lineDescentNeedsCollapsing(m_runList)) {
m_lineBox.shrinkVertically(m_lineBox.baseline().descent());
m_lineBox.resetDescent();
}
auto& layoutState = this->layoutState();
auto& formattingContext = this->formattingContext();
for (auto& run : m_runList) {
LayoutUnit logicalTop;
auto& layoutBox = run->layoutBox();
auto verticalAlign = layoutBox.style().verticalAlign();
auto ascent = layoutBox.style().fontMetrics().ascent();
switch (verticalAlign) {
case VerticalAlign::Baseline:
if (run->isLineBreak() || run->isText())
logicalTop = baselineOffset() - ascent;
else if (run->isContainerStart()) {
auto& boxGeometry = formattingContext.geometryForBox(layoutBox);
logicalTop = baselineOffset() - ascent - boxGeometry.borderTop() - boxGeometry.paddingTop().valueOr(0);
} else if (layoutBox.isInlineBlockBox() && layoutBox.establishesInlineFormattingContext()) {
auto& formattingState = downcast<InlineFormattingState>(layoutState.establishedFormattingState(downcast<Container>(layoutBox)));
// Spec makes us generate at least one line -even if it is empty.
ASSERT(!formattingState.lineBoxes().isEmpty());
auto inlineBlockBaselineOffset = formattingState.lineBoxes().last()->baselineOffset();
// The inline-block's baseline offset is relative to its content box. Let's convert it relative to the margin box.
// inline-block
// \
// _______________ <- margin box
// |
// | ____________ <- border box
// | |
// | | _________ <- content box
// | | | ^
// | | | | <- baseline offset
// | | | |
// text | | | v text
// -----|-|-|---------- <- baseline
//
auto& boxGeometry = formattingContext.geometryForBox(layoutBox);
auto baselineOffsetFromMarginBox = boxGeometry.marginBefore() + boxGeometry.borderTop() + boxGeometry.paddingTop().valueOr(0) + inlineBlockBaselineOffset;
logicalTop = baselineOffset() - baselineOffsetFromMarginBox;
} else
logicalTop = baselineOffset() - run->logicalRect().height();
break;
case VerticalAlign::Top:
logicalTop = { };
break;
case VerticalAlign::Bottom:
logicalTop = logicalBottom() - run->logicalRect().height();
break;
default:
ASSERT_NOT_IMPLEMENTED_YET();
break;
}
run->adjustLogicalTop(logicalTop);
// Convert runs from relative to the line top/left to the formatting root's border box top/left.
run->moveVertically(this->logicalTop());
run->moveHorizontally(this->logicalLeft());
}
}
void Line::alignContentHorizontally()
{
ASSERT(!m_skipAlignment);
auto adjustmentForAlignment = [&]() -> Optional<LayoutUnit> {
switch (*m_horizontalAlignment) {
case TextAlignMode::Left:
case TextAlignMode::WebKitLeft:
case TextAlignMode::Start:
return { };
case TextAlignMode::Right:
case TextAlignMode::WebKitRight:
case TextAlignMode::End:
return std::max(availableWidth(), 0_lu);
case TextAlignMode::Center:
case TextAlignMode::WebKitCenter:
return std::max(availableWidth() / 2, 0_lu);
case TextAlignMode::Justify:
ASSERT_NOT_REACHED();
break;
}
ASSERT_NOT_REACHED();
return { };
};
auto adjustment = adjustmentForAlignment();
if (!adjustment)
return;
for (auto& run : m_runList)
run->moveHorizontally(*adjustment);
// FIXME: Find out if m_lineBox needs adjustmnent as well.
}
void Line::removeTrailingTrimmableContent()
{
// Collapse trimmable trailing content
LayoutUnit trimmableWidth;
for (auto* trimmableRun : m_trimmableContent) {
ASSERT(trimmableRun->isText());
trimmableRun->setVisuallyIsEmpty();
trimmableWidth += trimmableRun->logicalRect().width();
}
m_lineBox.shrinkHorizontally(trimmableWidth);
}
void Line::moveLogicalLeft(LayoutUnit delta)
{
if (!delta)
return;
ASSERT(delta > 0);
m_lineBox.moveHorizontally(delta);
m_lineLogicalWidth -= delta;
}
void Line::moveLogicalRight(LayoutUnit delta)
{
ASSERT(delta > 0);
m_lineLogicalWidth -= delta;
}
LayoutUnit Line::trailingTrimmableWidth() const
{
LayoutUnit trimmableWidth;
for (auto* trimmableRun : m_trimmableContent) {
ASSERT(!trimmableRun->isVisuallyEmpty());
trimmableWidth += trimmableRun->logicalRect().width();
}
return trimmableWidth;
}
void Line::append(const InlineItem& inlineItem, LayoutUnit logicalWidth)
{
if (inlineItem.isHardLineBreak())
return appendHardLineBreak(inlineItem);
if (is<InlineTextItem>(inlineItem))
return appendTextContent(downcast<InlineTextItem>(inlineItem), logicalWidth);
if (inlineItem.isContainerStart())
return appendInlineContainerStart(inlineItem, logicalWidth);
if (inlineItem.isContainerEnd())
return appendInlineContainerEnd(inlineItem, logicalWidth);
if (inlineItem.layoutBox().replaced())
return appendReplacedInlineBox(inlineItem, logicalWidth);
appendNonReplacedInlineBox(inlineItem, logicalWidth);
}
void Line::appendNonBreakableSpace(const InlineItem& inlineItem, const Display::Rect& logicalRect)
{
m_runList.append(makeUnique<Run>(inlineItem, Display::Run { inlineItem.style(), logicalRect }));
m_lineBox.expandHorizontally(logicalRect.width());
}
void Line::appendInlineContainerStart(const InlineItem& inlineItem, LayoutUnit logicalWidth)
{
// This is really just a placeholder to mark the start of the inline level container <span>.
auto logicalRect = Display::Rect { 0, contentLogicalWidth(), logicalWidth, 0 };
if (!m_skipAlignment) {
adjustBaselineAndLineHeight(inlineItem);
logicalRect.setHeight(inlineItemContentHeight(inlineItem));
}
appendNonBreakableSpace(inlineItem, logicalRect);
}
void Line::appendInlineContainerEnd(const InlineItem& inlineItem, LayoutUnit logicalWidth)
{
// This is really just a placeholder to mark the end of the inline level container </span>.
auto logicalRect = Display::Rect { 0, contentLogicalRight(), logicalWidth, m_skipAlignment ? LayoutUnit() : inlineItemContentHeight(inlineItem) };
appendNonBreakableSpace(inlineItem, logicalRect);
}
void Line::appendTextContent(const InlineTextItem& inlineItem, LayoutUnit logicalWidth)
{
auto isTrimmable = TextUtil::isTrimmableContent(inlineItem);
if (!isTrimmable)
m_trimmableContent.clear();
auto willCollapseCompletely = [&] {
// Empty run.
if (!inlineItem.length()) {
ASSERT(!logicalWidth);
return true;
}
if (!isTrimmable)
return false;
// Leading whitespace.
if (m_runList.isEmpty())
return true;
// Check if the last item is trimmable as well.
for (int index = m_runList.size() - 1; index >= 0; --index) {
auto& run = m_runList[index];
if (run->isBox())
return false;
if (run->isText())
return run->isWhitespace() && run->layoutBox().style().collapseWhiteSpace();
ASSERT(run->isContainerStart() || run->isContainerEnd());
}
return true;
};
auto logicalRect = Display::Rect { };
logicalRect.setLeft(contentLogicalWidth());
logicalRect.setWidth(logicalWidth);
if (!m_skipAlignment) {
adjustBaselineAndLineHeight(inlineItem);
logicalRect.setHeight(inlineItemContentHeight(inlineItem));
}
auto contentStart = inlineItem.start();
auto contentLength = inlineItem.isCollapsed() ? 1 : inlineItem.length();
auto textContent = inlineItem.layoutBox().textContent().substring(contentStart, contentLength);
auto lineItem = makeUnique<Run>(inlineItem, Display::Run { inlineItem.style(), logicalRect, Display::Run::TextContext { contentStart, contentLength, textContent } });
auto isVisuallyEmpty = willCollapseCompletely();
if (isVisuallyEmpty)
lineItem->setVisuallyIsEmpty();
else if (isTrimmable)
m_trimmableContent.add(lineItem.get());
m_runList.append(WTFMove(lineItem));
// Collapsed line items don't contribute to the line width.
if (!isVisuallyEmpty)
m_lineBox.expandHorizontally(logicalWidth);
}
void Line::appendNonReplacedInlineBox(const InlineItem& inlineItem, LayoutUnit logicalWidth)
{
auto& boxGeometry = formattingContext().geometryForBox(inlineItem.layoutBox());
auto horizontalMargin = boxGeometry.horizontalMargin();
auto logicalRect = Display::Rect { };
logicalRect.setLeft(contentLogicalWidth() + horizontalMargin.start);
logicalRect.setWidth(logicalWidth);
if (!m_skipAlignment) {
adjustBaselineAndLineHeight(inlineItem);
auto runHeight = formattingContext().geometryForBox(inlineItem.layoutBox()).marginBoxHeight();
logicalRect.setHeight(runHeight);
}
m_runList.append(makeUnique<Run>(inlineItem, Display::Run { inlineItem.style(), logicalRect }));
m_lineBox.expandHorizontally(logicalWidth + horizontalMargin.start + horizontalMargin.end);
m_trimmableContent.clear();
}
void Line::appendReplacedInlineBox(const InlineItem& inlineItem, LayoutUnit logicalWidth)
{
ASSERT(inlineItem.layoutBox().isReplaced());
// FIXME: Surely replaced boxes behave differently.
appendNonReplacedInlineBox(inlineItem, logicalWidth);
if (auto* replaced = inlineItem.layoutBox().replaced(); replaced && replaced->cachedImage())
m_runList.last()->m_displayRun.setImage(*replaced->cachedImage());
}
void Line::appendHardLineBreak(const InlineItem& inlineItem)
{
auto logicalRect = Display::Rect { };
logicalRect.setLeft(contentLogicalWidth());
logicalRect.setWidth({ });
if (!m_skipAlignment) {
adjustBaselineAndLineHeight(inlineItem);
logicalRect.setHeight(logicalHeight());
}
m_runList.append(makeUnique<Run>(inlineItem, Display::Run { inlineItem.style(), logicalRect }));
}
void Line::adjustBaselineAndLineHeight(const InlineItem& inlineItem)
{
ASSERT(!inlineItem.isContainerEnd());
auto& layoutBox = inlineItem.layoutBox();
auto& style = layoutBox.style();
auto& baseline = m_lineBox.baseline();
if (inlineItem.isContainerStart()) {
// Inline containers stretch the line by their font size.
// Vertical margins, paddings and borders don't contribute to the line height.
auto& fontMetrics = style.fontMetrics();
if (style.verticalAlign() == VerticalAlign::Baseline) {
auto halfLeading = halfLeadingMetrics(fontMetrics, style.computedLineHeight());
// Both halfleading ascent and descent could be negative (tall font vs. small line-height value)
if (halfLeading.descent() > 0)
m_lineBox.setDescentIfGreater(halfLeading.descent());
if (halfLeading.ascent() > 0)
m_lineBox.setAscentIfGreater(halfLeading.ascent());
m_lineBox.setLogicalHeightIfGreater(baseline.height());
} else
m_lineBox.setLogicalHeightIfGreater(fontMetrics.height());
return;
}
if (inlineItem.isText() || inlineItem.isHardLineBreak()) {
// For text content we set the baseline either through the initial strut (set by the formatting context root) or
// through the inline container (start) -see above. Normally the text content itself does not stretch the line.
if (!m_initialStrut)
return;
m_lineBox.setAscentIfGreater(m_initialStrut->ascent());
m_lineBox.setDescentIfGreater(m_initialStrut->descent());
m_lineBox.setLogicalHeightIfGreater(baseline.height());
m_initialStrut = { };
return;
}
if (inlineItem.isBox()) {
auto& boxGeometry = formattingContext().geometryForBox(layoutBox);
auto marginBoxHeight = boxGeometry.marginBoxHeight();
switch (style.verticalAlign()) {
case VerticalAlign::Baseline: {
if (layoutBox.isInlineBlockBox() && layoutBox.establishesInlineFormattingContext()) {
// Inline-blocks with inline content always have baselines.
auto& formattingState = downcast<InlineFormattingState>(layoutState().establishedFormattingState(downcast<Container>(layoutBox)));
// Spec makes us generate at least one line -even if it is empty.
ASSERT(!formattingState.lineBoxes().isEmpty());
auto& lastLineBox = *formattingState.lineBoxes().last();
auto inlineBlockBaseline = lastLineBox.baseline();
auto beforeHeight = boxGeometry.marginBefore() + boxGeometry.borderTop() + boxGeometry.paddingTop().valueOr(0);
m_lineBox.setAscentIfGreater(inlineBlockBaseline.ascent());
m_lineBox.setDescentIfGreater(inlineBlockBaseline.descent());
m_lineBox.setBaselineOffsetIfGreater(beforeHeight + lastLineBox.baselineOffset());
m_lineBox.setLogicalHeightIfGreater(marginBoxHeight);
} else {
// Non inline-block boxes sit on the baseline (including their bottom margin).
m_lineBox.setAscentIfGreater(marginBoxHeight);
// Ignore negative descent (yes, negative descent is a thing).
m_lineBox.setLogicalHeightIfGreater(marginBoxHeight + std::max(LayoutUnit(), baseline.descent()));
}
break;
}
case VerticalAlign::Top:
// Top align content never changes the baseline, it only pushes the bottom of the line further down.
m_lineBox.setLogicalHeightIfGreater(marginBoxHeight);
break;
case VerticalAlign::Bottom: {
// Bottom aligned, tall content pushes the baseline further down from the line top.
auto lineLogicalHeight = m_lineBox.logicalHeight();
if (marginBoxHeight > lineLogicalHeight) {
m_lineBox.setLogicalHeightIfGreater(marginBoxHeight);
m_lineBox.setBaselineOffsetIfGreater(m_lineBox.baselineOffset() + (marginBoxHeight - lineLogicalHeight));
}
break;
}
default:
ASSERT_NOT_IMPLEMENTED_YET();
break;
}
return;
}
ASSERT_NOT_REACHED();
}
LayoutUnit Line::inlineItemContentHeight(const InlineItem& inlineItem) const
{
ASSERT(!m_skipAlignment);
auto& fontMetrics = inlineItem.style().fontMetrics();
if (inlineItem.isLineBreak() || is<InlineTextItem>(inlineItem))
return fontMetrics.height();
auto& layoutBox = inlineItem.layoutBox();
auto& boxGeometry = formattingContext().geometryForBox(layoutBox);
if (layoutBox.replaced() || layoutBox.isFloatingPositioned())
return boxGeometry.contentBoxHeight();
if (inlineItem.isContainerStart() || inlineItem.isContainerEnd())
return fontMetrics.height();
// Non-replaced inline box (e.g. inline-block). It looks a bit misleading but their margin box is considered the content height here.
return boxGeometry.marginBoxHeight();
}
LineBox::Baseline Line::halfLeadingMetrics(const FontMetrics& fontMetrics, LayoutUnit lineLogicalHeight)
{
auto ascent = fontMetrics.ascent();
auto descent = fontMetrics.descent();
// 10.8.1 Leading and half-leading
auto leading = lineLogicalHeight - (ascent + descent);
// Inline tree is all integer based.
auto adjustedAscent = std::max((ascent + leading / 2).floor(), 0);
auto adjustedDescent = std::max((descent + leading / 2).ceil(), 0);
return { adjustedAscent, adjustedDescent };
}
LayoutState& Line::layoutState() const
{
return formattingContext().layoutState();
}
const InlineFormattingContext& Line::formattingContext() const
{
return m_inlineFormattingContext;
}
}
}
#endif