blob: e86cb75d3ec637c14662ab0fe37cc865271a8703 [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 <wtf/IsoMallocInlines.h>
namespace WebCore {
namespace Layout {
WTF_MAKE_ISO_ALLOCATED_IMPL(Line);
Line::Content::Run::Run(const InlineItem& inlineItem, const Display::Rect& logicalRect)
: m_layoutBox(inlineItem.layoutBox())
, m_type(inlineItem.type())
, m_logicalRect(logicalRect)
{
}
Line::Content::Run::Run(const InlineItem& inlineItem, const TextContext& textContext, const Display::Rect& logicalRect)
: m_layoutBox(inlineItem.layoutBox())
, m_type(inlineItem.type())
, m_logicalRect(logicalRect)
, m_textContext(textContext)
{
}
Line::Line(const LayoutState& layoutState, const InitialConstraints& initialConstraints, SkipVerticalAligment skipVerticalAligment)
: m_layoutState(layoutState)
, m_content(std::make_unique<Line::Content>())
, m_logicalTopLeft(initialConstraints.topLeft)
, m_baseline({ initialConstraints.heightAndBaseline.baselineOffset, initialConstraints.heightAndBaseline.height - initialConstraints.heightAndBaseline.baselineOffset })
, m_initialStrut(initialConstraints.heightAndBaseline.strut)
, m_lineLogicalHeight(initialConstraints.heightAndBaseline.height)
, m_lineLogicalWidth(initialConstraints.availableWidth)
, m_skipVerticalAligment(skipVerticalAligment == SkipVerticalAligment::Yes)
{
}
static bool isInlineContainerConsideredEmpty(const LayoutState& layoutState, 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& displayBox = layoutState.displayBoxForLayoutBox(layoutBox);
return !(displayBox.horizontalBorder() || (displayBox.horizontalPadding() && displayBox.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>.
for (auto& run : m_content->runs()) {
if (run->isContainerStart()) {
if (!isInlineContainerConsideredEmpty(m_layoutState, run->layoutBox()))
return false;
continue;
}
if (run->isContainerEnd())
continue;
if (run->layoutBox().establishesFormattingContext()) {
ASSERT(run->layoutBox().isInlineBlockBox());
auto& displayBox = m_layoutState.displayBoxForLayoutBox(run->layoutBox());
if (!displayBox.width())
continue;
if (m_skipVerticalAligment || displayBox.height())
return false;
continue;
}
if (!run->textContext() || !run->textContext()->isCollapsed)
return false;
}
return true;
}
std::unique_ptr<Line::Content> Line::close()
{
removeTrailingTrimmableContent();
if (!m_skipVerticalAligment) {
if (isVisuallyEmpty()) {
m_baseline = { };
m_baselineTop = { };
m_lineLogicalHeight = { };
}
// Remove descent when all content is baseline aligned but none of them have descent.
if (InlineFormattingContext::Quirks::lineDescentNeedsCollapsing(m_layoutState, *m_content)) {
m_lineLogicalHeight -= m_baseline.descent;
m_baseline.descent = { };
}
for (auto& run : m_content->runs()) {
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& displayBox = m_layoutState.displayBoxForLayoutBox(layoutBox);
logicalTop = baselineOffset() - ascent - displayBox.borderTop() - displayBox.paddingTop().valueOr(0);
} else if (layoutBox.isInlineBlockBox() && layoutBox.establishesInlineFormattingContext()) {
auto& formattingState = downcast<InlineFormattingState>(m_layoutState.establishedFormattingState(layoutBox));
// Spec makes us generate at least one line -even if it is empty.
ASSERT(!formattingState.lineBoxes().isEmpty());
auto inlineBlockBaseline = formattingState.lineBoxes().last().baseline();
logicalTop = baselineOffset() - inlineBlockBaseline.ascent;
} 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());
}
}
m_content->setLogicalRect({ logicalTop(), logicalLeft(), contentLogicalWidth(), logicalHeight() });
m_content->setBaseline(m_baseline);
m_content->setBaselineOffset(baselineOffset());
return WTFMove(m_content);
}
void Line::removeTrailingTrimmableContent()
{
// Collapse trimmable trailing content
LayoutUnit trimmableWidth;
for (auto* trimmableRun : m_trimmableContent) {
ASSERT(trimmableRun->isText());
trimmableRun->setTextIsCollapsed();
trimmableWidth += trimmableRun->logicalRect().width();
}
m_contentLogicalWidth -= trimmableWidth;
}
void Line::moveLogicalLeft(LayoutUnit delta)
{
if (!delta)
return;
ASSERT(delta > 0);
// Shrink the line and move the items.
m_logicalTopLeft.move(delta, 0);
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->textContext()->isCollapsed);
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().isReplaced())
return appendReplacedInlineBox(inlineItem, logicalWidth);
appendNonReplacedInlineBox(inlineItem, logicalWidth);
}
void Line::appendNonBreakableSpace(const InlineItem& inlineItem, const Display::Rect& logicalRect)
{
m_content->runs().append(std::make_unique<Content::Run>(inlineItem, logicalRect));
m_contentLogicalWidth += logicalRect.width();
}
void Line::appendInlineContainerStart(const InlineItem& inlineItem, LayoutUnit logicalWidth)
{
auto logicalRect = Display::Rect { };
logicalRect.setLeft(contentLogicalWidth());
logicalRect.setWidth(logicalWidth);
if (!m_skipVerticalAligment) {
auto logicalHeight = inlineItemContentHeight(inlineItem);
adjustBaselineAndLineHeight(inlineItem, logicalHeight);
logicalRect.setHeight(logicalHeight);
}
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.
auto logicalRect = Display::Rect { 0, contentLogicalRight(), logicalWidth, 0 };
appendNonBreakableSpace(inlineItem, logicalRect);
}
void Line::appendTextContent(const InlineTextItem& inlineItem, LayoutUnit logicalWidth)
{
auto isTrimmable = TextUtil::isTrimmableContent(inlineItem);
if (!isTrimmable)
m_trimmableContent.clear();
auto shouldCollapseCompletely = [&] {
// Empty run.
if (!inlineItem.length()) {
ASSERT(!logicalWidth);
return true;
}
if (!isTrimmable)
return false;
// Leading whitespace.
auto& runs = m_content->runs();
if (runs.isEmpty())
return true;
// Check if the last item is trimmable as well.
for (int index = runs.size() - 1; index >= 0; --index) {
auto& run = runs[index];
if (run->isBox())
return false;
if (run->isText())
return run->textContext()->isWhitespace && run->layoutBox().style().collapseWhiteSpace();
ASSERT(run->isContainerStart() || run->isContainerEnd());
}
return true;
};
// Collapsed line items don't contribute to the line width.
auto isCompletelyCollapsed = shouldCollapseCompletely();
auto canBeExtended = !isCompletelyCollapsed && !inlineItem.isCollapsed();
auto logicalRect = Display::Rect { };
logicalRect.setLeft(contentLogicalWidth());
logicalRect.setWidth(logicalWidth);
if (!m_skipVerticalAligment) {
auto runHeight = inlineItemContentHeight(inlineItem);
logicalRect.setHeight(runHeight);
adjustBaselineAndLineHeight(inlineItem, runHeight);
}
auto textContext = Content::Run::TextContext { inlineItem.start(), inlineItem.isCollapsed() ? 1 : inlineItem.length(), isCompletelyCollapsed, canBeExtended, inlineItem.isWhitespace() };
auto lineItem = std::make_unique<Content::Run>(inlineItem, textContext, logicalRect);
if (isTrimmable && !isCompletelyCollapsed)
m_trimmableContent.add(lineItem.get());
m_content->runs().append(WTFMove(lineItem));
m_contentLogicalWidth += isCompletelyCollapsed ? LayoutUnit() : logicalWidth;
}
void Line::appendNonReplacedInlineBox(const InlineItem& inlineItem, LayoutUnit logicalWidth)
{
auto& displayBox = m_layoutState.displayBoxForLayoutBox(inlineItem.layoutBox());
auto horizontalMargin = displayBox.horizontalMargin();
auto logicalRect = Display::Rect { };
logicalRect.setLeft(contentLogicalWidth() + horizontalMargin.start);
logicalRect.setWidth(logicalWidth);
if (!m_skipVerticalAligment) {
adjustBaselineAndLineHeight(inlineItem, displayBox.marginBoxHeight());
logicalRect.setHeight(inlineItemContentHeight(inlineItem));
}
m_content->runs().append(std::make_unique<Content::Run>(inlineItem, logicalRect));
m_contentLogicalWidth += (logicalWidth + horizontalMargin.start + horizontalMargin.end);
m_trimmableContent.clear();
}
void Line::appendReplacedInlineBox(const InlineItem& inlineItem, LayoutUnit logicalWidth)
{
// FIXME Surely replaced boxes behave differently.
appendNonReplacedInlineBox(inlineItem, logicalWidth);
}
void Line::appendHardLineBreak(const InlineItem& inlineItem)
{
auto logicalRect = Display::Rect { };
logicalRect.setLeft(contentLogicalWidth());
logicalRect.setWidth({ });
if (!m_skipVerticalAligment) {
adjustBaselineAndLineHeight(inlineItem, { });
logicalRect.setHeight(logicalHeight());
}
m_content->runs().append(std::make_unique<Content::Run>(inlineItem, logicalRect));
}
void Line::adjustBaselineAndLineHeight(const InlineItem& inlineItem, LayoutUnit runHeight)
{
ASSERT(!inlineItem.isContainerEnd());
auto& layoutBox = inlineItem.layoutBox();
auto& style = layoutBox.style();
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();
LayoutUnit contentHeight;
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_baseline.descent = std::max(m_baseline.descent, halfLeading.descent);
if (halfLeading.ascent > 0)
m_baseline.ascent = std::max(m_baseline.ascent, halfLeading.ascent);
contentHeight = m_baseline.height();
} else
contentHeight = fontMetrics.height();
m_lineLogicalHeight = std::max(m_lineLogicalHeight, contentHeight);
m_baselineTop = std::max(m_baselineTop, m_lineLogicalHeight - m_baseline.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_baseline.ascent = std::max(m_initialStrut->ascent, m_baseline.ascent);
m_baseline.descent = std::max(m_initialStrut->descent, m_baseline.descent);
m_lineLogicalHeight = std::max(m_lineLogicalHeight, m_baseline.height());
m_baselineTop = std::max(m_baselineTop, m_lineLogicalHeight - m_baseline.height());
m_initialStrut = { };
return;
}
if (inlineItem.isBox()) {
switch (style.verticalAlign()) {
case VerticalAlign::Baseline: {
auto newBaselineCandidate = LineBox::Baseline { runHeight, 0 };
if (layoutBox.isInlineBlockBox() && layoutBox.establishesInlineFormattingContext()) {
// Inline-blocks with inline content always have baselines.
auto& formattingState = downcast<InlineFormattingState>(m_layoutState.establishedFormattingState(layoutBox));
// Spec makes us generate at least one line -even if it is empty.
ASSERT(!formattingState.lineBoxes().isEmpty());
newBaselineCandidate = formattingState.lineBoxes().last().baseline();
}
m_baseline.ascent = std::max(newBaselineCandidate.ascent, m_baseline.ascent);
m_baseline.descent = std::max(newBaselineCandidate.descent, m_baseline.descent);
m_lineLogicalHeight = std::max(std::max(m_lineLogicalHeight, runHeight), m_baseline.height());
// Baseline ascent/descent never shrink -> max.
m_baselineTop = std::max(m_baselineTop, m_lineLogicalHeight - m_baseline.height());
break;
}
case VerticalAlign::Top:
// Top align content never changes the baseline offset, it only pushes the bottom of the line further down.
m_lineLogicalHeight = std::max(runHeight, m_lineLogicalHeight);
break;
case VerticalAlign::Bottom:
// Bottom aligned, tall content pushes the baseline further down from the line top.
if (runHeight > m_lineLogicalHeight) {
m_baselineTop += (runHeight - m_lineLogicalHeight);
m_lineLogicalHeight = runHeight;
}
break;
default:
ASSERT_NOT_IMPLEMENTED_YET();
break;
}
return;
}
ASSERT_NOT_REACHED();
}
LayoutUnit Line::inlineItemContentHeight(const InlineItem& inlineItem) const
{
ASSERT(!m_skipVerticalAligment);
auto& fontMetrics = inlineItem.style().fontMetrics();
if (inlineItem.isLineBreak() || is<InlineTextItem>(inlineItem))
return fontMetrics.height();
auto& layoutBox = inlineItem.layoutBox();
ASSERT(m_layoutState.hasDisplayBox(layoutBox));
auto& displayBox = m_layoutState.displayBoxForLayoutBox(layoutBox);
if (layoutBox.isFloatingPositioned())
return displayBox.borderBoxHeight();
if (layoutBox.isReplaced())
return displayBox.borderBoxHeight();
if (inlineItem.isContainerStart() || inlineItem.isContainerEnd())
return fontMetrics.height() + displayBox.verticalBorder() + displayBox.verticalPadding().valueOr(0);
// Non-replaced inline box (e.g. inline-block)
return displayBox.borderBoxHeight();
}
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 };
}
}
}
#endif