blob: a60488e12bd912ef1ade9aba651a22503c78f1e8 [file] [log] [blame]
/*
* Copyright (C) 2021 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 "InlineLineBoxBuilder.h"
#include "InlineLineBoxVerticalAligner.h"
#include "InlineLineBuilder.h"
#include "LayoutBoxGeometry.h"
#include "LayoutReplacedBox.h"
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
namespace WebCore {
namespace Layout {
static std::optional<InlineLayoutUnit> horizontalAlignmentOffset(TextAlignMode textAlign, const LineBuilder::LineContent& lineContent)
{
// Depending on the line’s alignment/justification, the hanging glyph can be placed outside the line box.
auto& runs = lineContent.runs;
auto contentLogicalWidth = lineContent.contentLogicalWidth;
if (lineContent.hangingWhitespaceWidth) {
ASSERT(!runs.isEmpty());
// If white-space is set to pre-wrap, the UA must (unconditionally) hang this sequence, unless the sequence is followed
// by a forced line break, in which case it must conditionally hang the sequence is instead.
// Note that end of last line in a paragraph is considered a forced break.
auto isConditionalHanging = runs.last().isLineBreak() || lineContent.isLastLineWithInlineContent;
// In some cases, a glyph at the end of a line can conditionally hang: it hangs only if it does not otherwise fit in the line prior to justification.
// FIXME: Only the overflowing glyphs should be considered for hanging.
if (isConditionalHanging)
contentLogicalWidth = std::min(contentLogicalWidth, lineContent.lineLogicalWidth);
else
contentLogicalWidth -= lineContent.hangingWhitespaceWidth;
}
auto extraHorizontalSpace = lineContent.lineLogicalWidth - contentLogicalWidth;
if (extraHorizontalSpace <= 0)
return { };
auto computedHorizontalAlignment = [&] {
if (textAlign != TextAlignMode::Justify)
return textAlign;
// Text is justified according to the method specified by the text-justify property,
// in order to exactly fill the line box. Unless otherwise specified by text-align-last,
// the last line before a forced break or the end of the block is start-aligned.
if (lineContent.isLastLineWithInlineContent || (!runs.isEmpty() && runs.last().isLineBreak()))
return TextAlignMode::Start;
return TextAlignMode::Justify;
};
switch (computedHorizontalAlignment()) {
case TextAlignMode::Left:
case TextAlignMode::WebKitLeft:
case TextAlignMode::Start:
return { };
case TextAlignMode::Right:
case TextAlignMode::WebKitRight:
case TextAlignMode::End:
return extraHorizontalSpace;
case TextAlignMode::Center:
case TextAlignMode::WebKitCenter:
return extraHorizontalSpace / 2;
case TextAlignMode::Justify:
// TextAlignMode::Justify is a run alignment (and we only do inline box alignment here)
return { };
default:
ASSERT_NOT_IMPLEMENTED_YET();
return { };
}
ASSERT_NOT_REACHED();
return { };
}
LineBoxBuilder::LineBoxBuilder(const InlineFormattingContext& inlineFormattingContext)
: m_inlineFormattingContext(inlineFormattingContext)
{
}
LineBoxBuilder::LineAndLineBox LineBoxBuilder::build(const LineBuilder::LineContent& lineContent, size_t lineIndex)
{
auto textAlign = !lineIndex ? rootBox().firstLineStyle().textAlign() : rootBox().style().textAlign();
auto contentLogicalLeft = Layout::horizontalAlignmentOffset(textAlign, lineContent).value_or(InlineLayoutUnit { });
auto lineBox = LineBox { rootBox(), contentLogicalLeft, lineContent.contentLogicalWidth, lineIndex, lineContent.nonSpanningInlineLevelBoxCount };
auto lineBoxLogicalHeight = constructAndAlignInlineLevelBoxes(lineBox, lineContent.runs, lineIndex);
auto line = [&] {
auto lineBoxLogicalRect = InlineRect { lineContent.logicalTopLeft, lineContent.lineLogicalWidth, lineBoxLogicalHeight };
auto scrollableOverflowRect = lineBoxLogicalRect;
auto& rootInlineBox = lineBox.rootInlineBox();
auto enclosingTopAndBottom = InlineDisplay::Line::EnclosingTopAndBottom { lineBoxLogicalRect.top() + rootInlineBox.logicalTop(), lineBoxLogicalRect.top() + rootInlineBox.logicalBottom() };
for (auto& inlineLevelBox : lineBox.nonRootInlineLevelBoxes()) {
if (!inlineLevelBox.isAtomicInlineLevelBox() && !inlineLevelBox.isInlineBox())
continue;
auto& layoutBox = inlineLevelBox.layoutBox();
auto borderBox = InlineRect { };
if (inlineLevelBox.isAtomicInlineLevelBox()) {
borderBox = lineBox.logicalBorderBoxForAtomicInlineLevelBox(layoutBox, formattingContext().geometryForBox(layoutBox));
borderBox.moveBy(lineBoxLogicalRect.topLeft());
} else if (inlineLevelBox.isInlineBox()) {
auto& boxGeometry = formattingContext().geometryForBox(layoutBox);
borderBox = lineBox.logicalBorderBoxForInlineBox(layoutBox, boxGeometry);
borderBox.moveBy(lineBoxLogicalRect.topLeft());
// Collect scrollable overflow from inline boxes. All other inline level boxes (e.g atomic inline level boxes) stretch the line.
auto hasScrollableContent = [&] {
// In standards mode, inline boxes always start with an imaginary strut.
return layoutState().inStandardsMode() || inlineLevelBox.hasContent() || boxGeometry.horizontalBorder() || (boxGeometry.horizontalPadding() && boxGeometry.horizontalPadding().value());
};
if (lineBox.hasContent() && hasScrollableContent()) {
// Empty lines (e.g. continuation pre/post blocks) don't expect scrollbar overflow.
scrollableOverflowRect.expandToContain(borderBox);
}
} else
ASSERT_NOT_REACHED();
enclosingTopAndBottom.top = std::min(enclosingTopAndBottom.top, borderBox.top());
enclosingTopAndBottom.bottom = std::max(enclosingTopAndBottom.bottom, borderBox.bottom());
}
return InlineDisplay::Line { lineBoxLogicalRect, scrollableOverflowRect, enclosingTopAndBottom, rootInlineBox.logicalTop() + rootInlineBox.baseline(), rootInlineBox.logicalLeft(), rootInlineBox.logicalWidth() };
};
return { line(), lineBox };
}
void LineBoxBuilder::adjustVerticalGeometryForInlineBoxWithFallbackFonts(InlineLevelBox& inlineBox, const TextUtil::FallbackFontList& fallbackFontsForContent) const
{
ASSERT(!fallbackFontsForContent.isEmpty());
ASSERT(inlineBox.isInlineBox());
if (!inlineBox.isPreferredLineHeightFontMetricsBased())
return;
// https://www.w3.org/TR/css-inline-3/#inline-height
// When the computed line-height is normal, the layout bounds of an inline box encloses all its glyphs, going from the highest A to the deepest D.
auto maxAscent = InlineLayoutUnit { };
auto maxDescent = InlineLayoutUnit { };
// If line-height computes to normal and either text-edge is leading or this is the root inline box,
// the font's line gap metric may also be incorporated into A and D by adding half to each side as half-leading.
// FIXME: We don't support the text-edge property yet, but its initial value is 'leading' which makes the line-gap adjustment always on.
auto isTextEdgeLeading = true;
auto shouldUseLineGapToAdjustAscentDescent = inlineBox.isRootInlineBox() || isTextEdgeLeading;
for (auto* font : fallbackFontsForContent) {
auto& fontMetrics = font->fontMetrics();
InlineLayoutUnit ascent = fontMetrics.ascent();
InlineLayoutUnit descent = fontMetrics.descent();
if (shouldUseLineGapToAdjustAscentDescent) {
auto logicalHeight = ascent + descent;
auto halfLineGap = (fontMetrics.lineSpacing() - logicalHeight) / 2;
ascent = ascent + halfLineGap;
descent = descent + halfLineGap;
}
maxAscent = std::max(maxAscent, ascent);
maxDescent = std::max(maxDescent, descent);
}
// We need floor/ceil to match legacy layout integral positioning.
auto layoutBounds = inlineBox.layoutBounds();
inlineBox.setLayoutBounds({ std::max(layoutBounds.ascent, floorf(maxAscent)), std::max(layoutBounds.descent, ceilf(maxDescent)) });
}
struct HeightAndLayoutBounds {
InlineLayoutUnit ascent { 0 };
InlineLayoutUnit descent { 0 };
InlineLevelBox::LayoutBounds layoutBounds { };
};
static auto computedHeightAndLayoutBounds(const FontMetrics& fontMetrics, std::optional<InlineLayoutUnit> preferredLineHeight)
{
InlineLayoutUnit ascent = fontMetrics.ascent();
InlineLayoutUnit descent = fontMetrics.descent();
auto logicalHeight = ascent + descent;
if (preferredLineHeight) {
// If line-height computes to normal and either text-edge is leading or this is the root inline box,
// the font’s line gap metric may also be incorporated into A and D by adding half to each side as half-leading.
// https://www.w3.org/TR/css-inline-3/#inline-height
// Since text-edge is not supported yet and the initial value is leading, we should just apply it to
// all inline boxes.
auto halfLeading = (*preferredLineHeight - logicalHeight) / 2;
return HeightAndLayoutBounds { ascent, descent, { ascent + halfLeading, descent + halfLeading } };
}
// Preferred line height is purely font metrics based (i.e glyphs stretch the line).
auto halfLineGap = (fontMetrics.lineSpacing() - logicalHeight) / 2;
return HeightAndLayoutBounds { ascent, descent, { ascent + halfLineGap, descent + halfLineGap } };
}
void LineBoxBuilder::setVerticalGeometryForLineBreakBox(InlineLevelBox& lineBreakBox, const InlineLevelBox& parentInlineBox) const
{
// We need floor/ceil to match legacy layout integral positioning.
ASSERT(lineBreakBox.isLineBreakBox());
ASSERT(parentInlineBox.isInlineBox());
auto& fontMetrics = parentInlineBox.primaryFontMetrics();
auto preferredLineHeight = parentInlineBox.isPreferredLineHeightFontMetricsBased() ? std::nullopt : std::make_optional(parentInlineBox.preferredLineHeight());
auto heightAndLayoutBounds = computedHeightAndLayoutBounds(fontMetrics, preferredLineHeight);
lineBreakBox.setBaseline(floorf(heightAndLayoutBounds.ascent));
lineBreakBox.setDescent(ceilf(heightAndLayoutBounds.descent));
lineBreakBox.setLogicalHeight(heightAndLayoutBounds.ascent + heightAndLayoutBounds.descent);
lineBreakBox.setLayoutBounds({ floorf(heightAndLayoutBounds.layoutBounds.ascent), ceilf(heightAndLayoutBounds.layoutBounds.descent) });
}
void LineBoxBuilder::setInitialVerticalGeometryForInlineBox(InlineLevelBox& inlineBox) const
{
// We need floor/ceil to match legacy layout integral positioning.
ASSERT(inlineBox.isInlineBox());
auto& fontMetrics = inlineBox.primaryFontMetrics();
auto preferredLineHeight = inlineBox.isPreferredLineHeightFontMetricsBased() ? std::nullopt : std::make_optional(inlineBox.preferredLineHeight());
auto heightAndLayoutBounds = computedHeightAndLayoutBounds(fontMetrics, preferredLineHeight);
inlineBox.setBaseline(floorf(heightAndLayoutBounds.ascent));
inlineBox.setDescent(ceilf(heightAndLayoutBounds.descent));
inlineBox.setLogicalHeight(heightAndLayoutBounds.ascent + heightAndLayoutBounds.descent);
inlineBox.setLayoutBounds({ floorf(heightAndLayoutBounds.layoutBounds.ascent), ceilf(heightAndLayoutBounds.layoutBounds.descent) });
}
InlineLayoutUnit LineBoxBuilder::constructAndAlignInlineLevelBoxes(LineBox& lineBox, const Line::RunList& runs, size_t lineIndex)
{
auto& rootInlineBox = lineBox.rootInlineBox();
setInitialVerticalGeometryForInlineBox(rootInlineBox);
// FIXME: Add fast path support for line-height content.
// FIXME: We should always be able to exercise the fast path when the line has no content at all, even in non-standards mode or with line-height set.
auto canUseSimplifiedAlignment = layoutState().inStandardsMode() && rootInlineBox.isPreferredLineHeightFontMetricsBased();
auto updateCanUseSimplifiedAlignment = [&](auto& inlineLevelBox, std::optional<const BoxGeometry> boxGeometry = std::nullopt) {
if (!canUseSimplifiedAlignment)
return;
canUseSimplifiedAlignment = LineBoxVerticalAligner::canUseSimplifiedAlignmentForInlineLevelBox(rootInlineBox, inlineLevelBox, boxGeometry);
};
auto styleToUse = [&] (const auto& layoutBox) -> const RenderStyle& {
return !lineIndex ? layoutBox.firstLineStyle() : layoutBox.style();
};
auto lineHasContent = false;
for (auto& run : runs) {
auto& layoutBox = run.layoutBox();
auto& style = styleToUse(layoutBox);
auto runHasContent = [&] () -> bool {
ASSERT(!lineHasContent);
if (run.isText() || run.isBox() || run.isSoftLineBreak() || run.isHardLineBreak())
return true;
if (run.isLineSpanningInlineBoxStart())
return false;
if (run.isWordBreakOpportunity())
return false;
auto& inlineBoxGeometry = formattingContext().geometryForBox(layoutBox);
// Even negative horizontal margin makes the line "contentful".
if (run.isInlineBoxStart())
return inlineBoxGeometry.marginStart() || inlineBoxGeometry.borderLeft() || inlineBoxGeometry.paddingLeft().value_or(0_lu);
if (run.isInlineBoxEnd())
return inlineBoxGeometry.marginEnd() || inlineBoxGeometry.borderRight() || inlineBoxGeometry.paddingRight().value_or(0_lu);
ASSERT_NOT_REACHED();
return true;
};
lineHasContent = lineHasContent || runHasContent();
auto logicalLeft = rootInlineBox.logicalLeft() + run.logicalLeft();
if (run.isBox()) {
auto& inlineLevelBoxGeometry = formattingContext().geometryForBox(layoutBox);
auto marginBoxHeight = inlineLevelBoxGeometry.marginBoxHeight();
auto ascent = InlineLayoutUnit { };
if (layoutState().shouldNotSynthesizeInlineBlockBaseline()) {
// Integration codepath constructs replaced boxes for inline-block content.
ASSERT(layoutBox.isReplacedBox());
ascent = *downcast<ReplacedBox>(layoutBox).baseline();
} else if (layoutBox.isInlineBlockBox()) {
// The baseline of an 'inline-block' is the baseline of its last line box in the normal flow, unless it has either no in-flow line boxes or
// if its 'overflow' property has a computed value other than 'visible', in which case the baseline is the bottom margin edge.
auto synthesizeBaseline = !layoutBox.establishesInlineFormattingContext() || !style.isOverflowVisible();
if (synthesizeBaseline)
ascent = marginBoxHeight;
else {
auto& formattingState = layoutState().formattingStateForInlineFormattingContext(downcast<ContainerBox>(layoutBox));
auto& lastLine = formattingState.lines().last();
auto inlineBlockBaseline = lastLine.lineBoxLogicalRect().top() + lastLine.baseline();
ascent = inlineLevelBoxGeometry.marginBefore() + inlineLevelBoxGeometry.borderTop() + inlineLevelBoxGeometry.paddingTop().value_or(0) + inlineBlockBaseline;
}
} else if (layoutBox.isReplacedBox())
ascent = downcast<ReplacedBox>(layoutBox).baseline().value_or(marginBoxHeight);
else
ascent = marginBoxHeight;
logicalLeft += std::max(0_lu, inlineLevelBoxGeometry.marginStart());
auto atomicInlineLevelBox = InlineLevelBox::createAtomicInlineLevelBox(layoutBox, style, logicalLeft, { inlineLevelBoxGeometry.borderBoxWidth(), marginBoxHeight });
atomicInlineLevelBox.setBaseline(ascent);
atomicInlineLevelBox.setLayoutBounds(InlineLevelBox::LayoutBounds { ascent, marginBoxHeight - ascent });
updateCanUseSimplifiedAlignment(atomicInlineLevelBox, inlineLevelBoxGeometry);
lineBox.addInlineLevelBox(WTFMove(atomicInlineLevelBox));
continue;
}
if (run.isLineSpanningInlineBoxStart()) {
auto marginStart = InlineLayoutUnit { };
#if ENABLE(CSS_BOX_DECORATION_BREAK)
if (style.boxDecorationBreak() == BoxDecorationBreak::Clone)
marginStart = formattingContext().geometryForBox(layoutBox).marginStart();
#endif
auto adjustedLogicalStart = logicalLeft + marginStart;
auto logicalWidth = rootInlineBox.logicalWidth() - adjustedLogicalStart;
auto inlineBox = InlineLevelBox::createInlineBox(layoutBox, style, adjustedLogicalStart, logicalWidth, InlineLevelBox::LineSpanningInlineBox::Yes);
setInitialVerticalGeometryForInlineBox(inlineBox);
updateCanUseSimplifiedAlignment(inlineBox);
lineBox.addInlineLevelBox(WTFMove(inlineBox));
continue;
}
if (run.isInlineBoxStart()) {
// At this point we don't know yet how wide this inline box is. Let's assume it's as long as the line is
// and adjust it later if we come across an inlineBoxEnd run (see below).
// Inline box run is based on margin box. Let's convert it to border box.
auto marginStart = formattingContext().geometryForBox(layoutBox).marginStart();
auto initialLogicalWidth = rootInlineBox.logicalWidth() - (run.logicalLeft() + marginStart);
ASSERT(initialLogicalWidth >= 0);
auto inlineBox = InlineLevelBox::createInlineBox(layoutBox, style, logicalLeft + marginStart, initialLogicalWidth);
inlineBox.setIsFirstBox();
setInitialVerticalGeometryForInlineBox(inlineBox);
updateCanUseSimplifiedAlignment(inlineBox);
lineBox.addInlineLevelBox(WTFMove(inlineBox));
continue;
}
if (run.isInlineBoxEnd()) {
// Adjust the logical width when the inline box closes on this line.
// Note that margin end does not affect the logical width (e.g. positive margin right does not make the run wider).
auto& inlineBox = lineBox.inlineLevelBoxForLayoutBox(layoutBox);
ASSERT(inlineBox.isInlineBox());
// Inline box run is based on margin box. Let's convert it to border box.
// Negative margin end makes the run have negative width.
auto marginEndAdjustemnt = -formattingContext().geometryForBox(layoutBox).marginEnd();
auto logicalWidth = run.logicalWidth() + marginEndAdjustemnt;
auto inlineBoxLogicalRight = logicalLeft + logicalWidth;
// When the content pulls the </span> to the logical left direction (e.g. negative letter space)
// make sure we don't end up with negative logical width on the inline box.
inlineBox.setLogicalWidth(std::max(0.f, inlineBoxLogicalRight - inlineBox.logicalLeft()));
inlineBox.setIsLastBox();
updateCanUseSimplifiedAlignment(inlineBox);
continue;
}
if (run.isText()) {
auto& parentInlineBox = lineBox.inlineLevelBoxForLayoutBox(layoutBox.parent());
parentInlineBox.setHasContent();
auto fallbackFonts = TextUtil::fallbackFontsForRun(run, style);
if (!fallbackFonts.isEmpty()) {
// Adjust non-empty inline box height when glyphs from the non-primary font stretch the box.
adjustVerticalGeometryForInlineBoxWithFallbackFonts(parentInlineBox, fallbackFonts);
updateCanUseSimplifiedAlignment(parentInlineBox);
}
continue;
}
if (run.isSoftLineBreak()) {
lineBox.inlineLevelBoxForLayoutBox(layoutBox.parent()).setHasContent();
continue;
}
if (run.isHardLineBreak()) {
auto lineBreakBox = InlineLevelBox::createLineBreakBox(layoutBox, style, logicalLeft);
auto& parentInlineBox = lineBox.inlineLevelBoxForLayoutBox(layoutBox.parent());
setVerticalGeometryForLineBreakBox(lineBreakBox, parentInlineBox);
updateCanUseSimplifiedAlignment(lineBreakBox);
lineBox.addInlineLevelBox(WTFMove(lineBreakBox));
continue;
}
if (run.isWordBreakOpportunity()) {
lineBox.addInlineLevelBox(InlineLevelBox::createGenericInlineLevelBox(layoutBox, style, logicalLeft));
continue;
}
ASSERT_NOT_REACHED();
}
lineBox.setHasContent(lineHasContent);
auto verticalAligner = LineBoxVerticalAligner { formattingContext() };
canUseSimplifiedAlignment = canUseSimplifiedAlignment || !lineHasContent;
return verticalAligner.computeLogicalHeightAndAlign(lineBox, canUseSimplifiedAlignment);
}
}
}
#endif