blob: cd7992da995e896372a9063989d5132c05ecb3c3 [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);
class InlineItemRun {
WTF_MAKE_ISO_ALLOCATED_INLINE(InlineItemRun);
public:
InlineItemRun(const InlineItem&, const Display::Rect&, WTF::Optional<Display::Run::TextContext> = WTF::nullopt);
const Box& layoutBox() const { return m_inlineItem.layoutBox(); }
const Display::Rect& logicalRect() const { return m_logicalRect; }
Optional<Display::Run::TextContext> textContext() const { return m_textContext; }
bool isText() const { return m_inlineItem.isText(); }
bool isBox() const { return m_inlineItem.isBox(); }
bool isContainerStart() const { return m_inlineItem.isContainerStart(); }
bool isContainerEnd() const { return m_inlineItem.isContainerEnd(); }
bool isForcedLineBreak() const { return m_inlineItem.isForcedLineBreak(); }
InlineItem::Type type() const { return m_inlineItem.type(); }
void setIsCollapsed() { m_isCollapsed = true; }
bool isCollapsed() const { return m_isCollapsed; }
void setCollapsesToZeroAdvanceWidth();
bool isCollapsedToZeroAdvanceWidth() const { return m_collapsedToZeroAdvanceWidth; }
bool isCollapsible() const { return is<InlineTextItem>(m_inlineItem) && downcast<InlineTextItem>(m_inlineItem).isCollapsible(); }
bool isWhitespace() const { return is<InlineTextItem>(m_inlineItem) && downcast<InlineTextItem>(m_inlineItem).isWhitespace(); }
bool hasExpansionOpportunity() const { return isWhitespace() && !isCollapsedToZeroAdvanceWidth(); }
private:
const InlineItem& m_inlineItem;
Display::Rect m_logicalRect;
const Optional<Display::Run::TextContext> m_textContext;
bool m_isCollapsed { false };
bool m_collapsedToZeroAdvanceWidth { false };
};
InlineItemRun::InlineItemRun(const InlineItem& inlineItem, const Display::Rect& logicalRect, WTF::Optional<Display::Run::TextContext> textContext)
: m_inlineItem(inlineItem)
, m_logicalRect(logicalRect)
, m_textContext(textContext)
{
}
void InlineItemRun::setCollapsesToZeroAdvanceWidth()
{
m_collapsedToZeroAdvanceWidth = true;
m_logicalRect.setWidth({ });
}
Line::Run::Run(const Box& layoutBox, InlineItem::Type type, const Display::Rect& logicalRect)
: m_layoutBox(layoutBox)
, m_type(type)
, m_logicalRect(logicalRect)
{
}
void Line::Run::adjustExpansionBehavior(ExpansionBehavior expansionBehavior)
{
ASSERT(isText());
ASSERT(hasExpansionOpportunity());
m_textContext->setExpansion({ expansionBehavior, m_textContext->expansion()->horizontalExpansion });
}
inline Optional<ExpansionBehavior> Line::Run::expansionBehavior() const
{
ASSERT(isText());
if (auto expansionContext = m_textContext->expansion())
return expansionContext->behavior;
return { };
}
void Line::Run::setHasExpansionOpportunity(ExpansionBehavior expansionBehavior)
{
ASSERT(isText());
ASSERT(!hasExpansionOpportunity());
m_expansionOpportunityCount = 1;
m_textContext->setExpansion({ expansionBehavior, { } });
}
void Line::Run::setComputedHorizontalExpansion(LayoutUnit logicalExpansion)
{
ASSERT(isText());
ASSERT(hasExpansionOpportunity());
m_logicalRect.expandHorizontally(logicalExpansion);
m_textContext->setExpansion({ m_textContext->expansion()->behavior, logicalExpansion });
}
void Line::Run::expand(const InlineItemRun& nextRun)
{
ASSERT(isText());
ASSERT(nextRun.isText());
ASSERT(!isCollapsedToVisuallyEmpty());
m_logicalRect.expandHorizontally(nextRun.logicalRect().width());
m_textContext->expand(*nextRun.textContext());
// FIXME: This is a very simple expansion merge. We should eventually switch over to FontCascade::expansionOpportunityCount.
if (hasExpansionOpportunity() && nextRun.hasExpansionOpportunity()) {
// Run is expanded with a whitespace content.
adjustExpansionBehavior(ForbidLeadingExpansion | AllowTrailingExpansion);
++m_expansionOpportunityCount;
} else if (!hasExpansionOpportunity() && nextRun.hasExpansionOpportunity()) {
// Nonwhitespace runs is expanded with whitespace.
setHasExpansionOpportunity(ForbidLeadingExpansion | AllowTrailingExpansion);
} else if (hasExpansionOpportunity() && !nextRun.hasExpansionOpportunity()) {
// Run is expanded with a nonwhitespace content.
adjustExpansionBehavior(AllowLeadingExpansion | AllowTrailingExpansion);
}
}
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 };
}
Line::~Line()
{
}
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()));
}
static bool shouldPreserveTrailingContent(const InlineTextItem& inlineTextItem)
{
if (!inlineTextItem.isWhitespace())
return true;
auto whitespace = inlineTextItem.style().whiteSpace();
return whitespace == WhiteSpace::Pre || whitespace == WhiteSpace::PreWrap;
}
static bool shouldPreserveLeadingContent(const InlineTextItem& inlineTextItem)
{
if (!inlineTextItem.isWhitespace())
return true;
auto whitespace = inlineTextItem.style().whiteSpace();
return whitespace == WhiteSpace::Pre || whitespace == WhiteSpace::PreWrap || whitespace == WhiteSpace::BreakSpaces;
}
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_inlineItemRuns) {
if (run->isText()) {
if (!run->isCollapsedToZeroAdvanceWidth())
return false;
continue;
}
if (run->isContainerStart()) {
if (!isInlineContainerConsideredEmpty(formattingContext, run->layoutBox()))
return false;
continue;
}
if (run->isContainerEnd())
continue;
if (run->isBox()) {
if (!run->layoutBox().establishesFormattingContext())
return false;
ASSERT(run->layoutBox().isInlineBlockBox());
auto& boxGeometry = formattingContext.geometryForBox(run->layoutBox());
if (!boxGeometry.width())
continue;
if (m_skipAlignment || boxGeometry.height())
return false;
continue;
}
if (run->isForcedLineBreak())
return false;
}
return true;
}
Line::RunList Line::close(IsLastLineWithInlineContent isLastLineWithInlineContent)
{
// 1. Remove trimmable trailing content.
// 2. Join text runs together when possible [foo][ ][bar] -> [foo bar].
// 3. Align merged runs both vertically and horizontally.
removeTrailingTrimmableContent();
RunList runList;
for (unsigned i = 0; i < m_inlineItemRuns.size(); ++i) {
auto constructRun = [&] (const auto& inlineItemRun) {
runList.append(Run { inlineItemRun.layoutBox(), inlineItemRun.type(), inlineItemRun.logicalRect() });
if (!inlineItemRun.isText())
return;
auto& run = runList.last();
if (inlineItemRun.isCollapsedToZeroAdvanceWidth())
run.setIsCollapsedToVisuallyEmpty();
if (auto textContext = inlineItemRun.textContext()) {
run.setTextContext(*textContext);
if (isTextAlignJustify() && inlineItemRun.hasExpansionOpportunity())
run.setHasExpansionOpportunity(DefaultExpansion);
}
};
constructRun(*m_inlineItemRuns[i]);
// Merge eligible runs.
while (i < m_inlineItemRuns.size() - 1) {
auto canMergeRuns = [] (const auto& currentRun, const auto& nextRun) {
// Do not merge runs across inline boxes (<span>foo</span><span>bar</span>)
if (&currentRun.layoutBox() != &nextRun.layoutBox())
return false;
// Only text content can be merged.
if (!currentRun.isText() || !nextRun.isText())
return false;
// Merged content needs to be continuous.
if (currentRun.isCollapsed())
return false;
// Visually empty runs are ignored.
if (currentRun.isCollapsedToZeroAdvanceWidth() || nextRun.isCollapsedToZeroAdvanceWidth())
return false;
return true;
};
auto& currentRun = m_inlineItemRuns[i];
auto& nextRun = m_inlineItemRuns[i + 1];
if (!canMergeRuns(*currentRun, *nextRun))
break;
runList.last().expand(*nextRun);
// Skip the merged run.
++i;
}
}
if (!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(runList)) {
m_lineBox.shrinkVertically(m_lineBox.baseline().descent());
m_lineBox.resetDescent();
}
alignContentVertically(runList);
alignContentHorizontally(runList, isLastLineWithInlineContent);
}
return runList;
}
void Line::alignContentVertically(RunList& runList) const
{
ASSERT(!m_skipAlignment);
for (auto& run : 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.isForcedLineBreak() || 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::justifyRuns(RunList& runList) const
{
ASSERT(!runList.isEmpty());
ASSERT(availableWidth() > 0);
// Need to fix up the last run first.
auto& lastRun = runList.last();
if (lastRun.hasExpansionOpportunity())
lastRun.adjustExpansionBehavior(*lastRun.expansionBehavior() | ForbidTrailingExpansion);
// Collect the expansion opportunity numbers.
auto expansionOpportunityCount = 0;
for (auto& run : runList)
expansionOpportunityCount += run.expansionOpportunityCount();
// Nothing to distribute?
if (!expansionOpportunityCount)
return;
// Distribute the extra space.
auto expansionToDistribute = availableWidth() / expansionOpportunityCount;
LayoutUnit accumulatedExpansion;
for (auto& run : runList) {
// Expand and moves runs by the accumulated expansion.
if (!run.hasExpansionOpportunity()) {
run.moveHorizontally(accumulatedExpansion);
continue;
}
ASSERT(run.expansionOpportunityCount());
auto computedExpansion = expansionToDistribute * run.expansionOpportunityCount();
run.setComputedHorizontalExpansion(computedExpansion);
run.moveHorizontally(accumulatedExpansion);
accumulatedExpansion += computedExpansion;
}
}
void Line::alignContentHorizontally(RunList& runList, IsLastLineWithInlineContent lastLine) const
{
ASSERT(!m_skipAlignment);
if (runList.isEmpty() || availableWidth() <= 0)
return;
if (isTextAlignJustify()) {
// Do not justify align the last line.
if (lastLine == IsLastLineWithInlineContent::No)
justifyRuns(runList);
return;
}
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 : runList)
run.moveHorizontally(*adjustment);
}
void Line::removeTrailingTrimmableContent()
{
// Collapse trimmable trailing content
LayoutUnit trimmableWidth;
for (auto* trimmableRun : m_trimmableRuns) {
ASSERT(trimmableRun->isText());
// FIXME: We might need to be able to differentiate between trimmed and collapsed runs.
trimmableWidth += trimmableRun->logicalRect().width();
trimmableRun->setCollapsesToZeroAdvanceWidth();
}
m_lineBox.shrinkHorizontally(trimmableWidth);
m_trimmableRuns.clear();
}
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_trimmableRuns)
trimmableWidth += trimmableRun->logicalRect().width();
return trimmableWidth;
}
void Line::append(const InlineItem& inlineItem, LayoutUnit logicalWidth)
{
if (inlineItem.isForcedLineBreak())
return appendLineBreak(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_inlineItemRuns.append(makeUnique<InlineItemRun>(inlineItem, logicalRect));
m_lineBox.expandHorizontally(logicalRect.width());
if (logicalRect.width())
m_lineBox.setIsConsideredNonEmpty();
}
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 = !shouldPreserveTrailingContent(inlineItem);
if (!isTrimmable)
m_trimmableRuns.clear();
auto willCollapseCompletely = [&] {
// Empty run.
if (!inlineItem.length()) {
ASSERT(!logicalWidth);
return true;
}
// Leading whitespace.
if (m_inlineItemRuns.isEmpty())
return !shouldPreserveLeadingContent(inlineItem);
if (!inlineItem.isCollapsible())
return false;
// Check if the last item is collapsed as well.
for (auto i = m_inlineItemRuns.size(); i--;) {
auto& run = m_inlineItemRuns[i];
if (run->isBox())
return false;
// https://drafts.csswg.org/css-text-3/#white-space-phase-1
// Any collapsible space immediately following another collapsible space—even one outside the boundary of the inline containing that space,
// provided both spaces are within the same inline formatting context—is collapsed to have zero advance width.
// : "<span> </span> " <- the trailing whitespace collapses completely.
// Not that when the inline container has preserve whitespace style, "<span style="white-space: pre"> </span> " <- this whitespace stays around.
if (run->isText())
return run->isCollapsible();
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 collapsedRun = inlineItem.isCollapsible() && inlineItem.length() > 1;
auto contentStart = inlineItem.start();
auto contentLength = collapsedRun ? 1 : inlineItem.length();
auto textContent = inlineItem.layoutBox().textContent().substring(contentStart, contentLength);
auto lineRun = makeUnique<InlineItemRun>(inlineItem, logicalRect, Display::Run::TextContext { contentStart, contentLength, textContent });
auto collapsesToZeroAdvanceWidth = willCollapseCompletely();
if (collapsesToZeroAdvanceWidth)
lineRun->setCollapsesToZeroAdvanceWidth();
else
m_lineBox.setIsConsideredNonEmpty();
if (collapsedRun)
lineRun->setIsCollapsed();
if (isTrimmable)
m_trimmableRuns.append(lineRun.get());
m_lineBox.expandHorizontally(lineRun->logicalRect().width());
m_inlineItemRuns.append(WTFMove(lineRun));
}
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_inlineItemRuns.append(makeUnique<InlineItemRun>(inlineItem, logicalRect));
m_lineBox.expandHorizontally(logicalWidth + horizontalMargin.start + horizontalMargin.end);
m_lineBox.setIsConsideredNonEmpty();
m_trimmableRuns.clear();
}
void Line::appendReplacedInlineBox(const InlineItem& inlineItem, LayoutUnit logicalWidth)
{
ASSERT(inlineItem.layoutBox().isReplaced());
// FIXME: Surely replaced boxes behave differently.
appendNonReplacedInlineBox(inlineItem, logicalWidth);
m_lineBox.setIsConsideredNonEmpty();
}
void Line::appendLineBreak(const InlineItem& inlineItem)
{
auto logicalRect = Display::Rect { };
logicalRect.setLeft(contentLogicalWidth());
logicalRect.setWidth({ });
if (!m_skipAlignment) {
adjustBaselineAndLineHeight(inlineItem);
logicalRect.setHeight(logicalHeight());
}
m_lineBox.setIsConsideredNonEmpty();
m_inlineItemRuns.append(makeUnique<InlineItemRun>(inlineItem, 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.isForcedLineBreak()) {
// 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.isForcedLineBreak() || 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