Skip to content
Permalink
Browse files
[LFC][IFC] text-overflow: ellipsis should not affect geometries
https://bugs.webkit.org/show_bug.cgi?id=244173

Reviewed by Antti Koivisto.

text-overflow: ellipsis: (https://drafts.csswg.org/css-overflow/#text-overflow)
- This property specifies .. its end line box edge in the inline progression direction ...
- Ellipsing only affects rendering and must not affect layout ..

1. Move content truncation from display box builder to line builder. Line builder uses logical direction while display box builder operates on visual geometry. Using logical direction has the benefit of keeping truncation direction independent. Line content truncation happens as the last step when "closing" the current line right after completing all the geometry affecting operations (trailing content trimming etc).

2. Do not adjust display boxes with the truncated content. Truncation should only affect rendering. Instead introduce a visibility flag which drives rendering when ellipsis (and truncation) is present.

* Source/WebCore/layout/formattingContexts/inline/InlineLine.cpp:
(WebCore::Layout::Line::truncate):
(WebCore::Layout::Line::Run::detachTrailingWhitespace):
(WebCore::Layout::Line::Run::truncate):
* Source/WebCore/layout/formattingContexts/inline/InlineLine.h:
(WebCore::Layout::Line::Run::isTruncated const):
* Source/WebCore/layout/formattingContexts/inline/InlineLineBuilder.cpp:
(WebCore::Layout::LineBuilder::close):
* Source/WebCore/layout/formattingContexts/inline/display/InlineDisplayBox.h:
(WebCore::InlineDisplay::Box::Text::visuallyVisibleLength const):
(WebCore::InlineDisplay::Box::Box):
(WebCore::InlineDisplay::Box::isVisuallyHidden const):
(WebCore::InlineDisplay::Box::adjustInkOverflow):
(WebCore::InlineDisplay::Box::Text::Text):
(WebCore::InlineDisplay::Box::Text::truncatedLength const): Deleted.
(WebCore::InlineDisplay::Box::truncate): Deleted.
* Source/WebCore/layout/formattingContexts/inline/display/InlineDisplayContentBuilder.cpp:
(WebCore::Layout::InlineDisplayContentBuilder::build):
(WebCore::Layout::InlineDisplayContentBuilder::appendTextDisplayBox):
(WebCore::Layout::InlineDisplayContentBuilder::appendAtomicInlineLevelDisplayBox):
(WebCore::Layout::InlineDisplayContentBuilder::appendInlineBoxDisplayBox):
(WebCore::Layout::InlineDisplayContentBuilder::appendSpanningInlineBoxDisplayBox):
(WebCore::Layout::InlineDisplayContentBuilder::processOverflownRunsForEllipsis):
* Source/WebCore/layout/formattingContexts/inline/display/InlineDisplayContentBuilder.h:
* Source/WebCore/layout/integration/inline/InlineIteratorBoxModernPath.h:
(WebCore::InlineIterator::BoxModernPath::selectableRange const):
* Source/WebCore/layout/integration/inline/LayoutIntegrationLineLayout.cpp:
(WebCore::LayoutIntegration::LineLayout::paint):

Canonical link: https://commits.webkit.org/253650@main
  • Loading branch information
alanbaradlay committed Aug 22, 2022
1 parent ba0ed9b commit dfaf8254ed07b79eedc29afe6d4e5f8b241be642
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 65 deletions.
@@ -172,6 +172,17 @@ void Line::applyRunExpansion(InlineLayoutUnit horizontalAvailableSpace)
m_contentLogicalWidth += accumulatedExpansion;
}

void Line::truncate(InlineLayoutUnit logicalRight)
{
ASSERT(m_contentLogicalWidth > logicalRight);
for (auto& run : m_runs) {
if (run.isInlineBox())
continue;
if (run.logicalRight() > logicalRight)
run.truncate(logicalRight - run.logicalLeft());
}
}

void Line::removeTrailingTrimmableContent(ShouldApplyTrailingWhiteSpaceFollowedByBRQuirk shouldApplyTrailingWhiteSpaceFollowedByBRQuirk)
{
if (m_trimmableTrailingContent.isEmpty() || m_runs.isEmpty())
@@ -693,7 +704,7 @@ std::optional<Line::Run> Line::Run::detachTrailingWhitespace()
auto trailingWhitespaceRun = *this;

auto leadingNonWhitespaceContentLength = m_textContent->length - m_trailingWhitespace->length;
trailingWhitespaceRun.m_textContent = { m_textContent->start + leadingNonWhitespaceContentLength, m_trailingWhitespace->length, false };
trailingWhitespaceRun.m_textContent = { m_textContent->start + leadingNonWhitespaceContentLength, m_trailingWhitespace->length, { }, false };

trailingWhitespaceRun.m_logicalWidth = m_trailingWhitespace->width;
trailingWhitespaceRun.m_logicalLeft = logicalRight() - m_trailingWhitespace->width;
@@ -755,6 +766,24 @@ InlineLayoutUnit Line::Run::removeTrailingWhitespace()
return trimmedWidth;
}

void Line::Run::truncate(InlineLayoutUnit visibledWidth)
{
ASSERT(!visibledWidth || visibledWidth < m_logicalWidth);
m_isTruncated = true;
if (!isText() || visibledWidth <= 0.f)
return;
auto leftSide = TextUtil::breakWord(downcast<InlineTextBox>(*m_layoutBox)
, m_textContent->start
, m_textContent->length
, m_logicalWidth
, visibledWidth
, m_logicalLeft
, m_style.fontCascade());

if (leftSide.length)
m_textContent->partiallyVisibleContent = { leftSide.length, leftSide.logicalWidth };
}

}
}

@@ -68,6 +68,7 @@ class Line {
void removeHangingGlyphs();
void resetBidiLevelForTrailingWhitespace(UBiDiLevel rootBidiLevel);
void applyRunExpansion(InlineLayoutUnit horizontalAvailableSpace);
void truncate(InlineLayoutUnit logicalRight);

struct Run {
enum class Type : uint8_t {
@@ -99,10 +100,17 @@ class Line {
bool isContentful() const { return (isText() && textContent()->length) || isBox() || isLineBreak() || isListMarker(); }
bool isGenerated() const { return isListMarker(); }

bool isTruncated() const { return m_isTruncated; }

const Box& layoutBox() const { return *m_layoutBox; }
struct Text {
size_t start { 0 };
size_t length { 0 };
struct PartiallyVisibleContent {
size_t length { 0 };
InlineLayoutUnit width { 0.f };
};
std::optional<PartiallyVisibleContent> partiallyVisibleContent { };
bool needsHyphen { false };
};
const std::optional<Text>& textContent() const { return m_textContent; }
@@ -160,6 +168,7 @@ class Line {
bool hasTrailingLetterSpacing() const;
InlineLayoutUnit trailingLetterSpacing() const;
InlineLayoutUnit removeTrailingLetterSpacing();
void truncate(InlineLayoutUnit truncatedWidth);

Type m_type { Type::Text };
const Box* m_layoutBox { nullptr };
@@ -171,6 +180,7 @@ class Line {
std::optional<TrailingWhitespace> m_trailingWhitespace { };
std::optional<size_t> m_lastNonWhitespaceContentStart { };
std::optional<Text> m_textContent;
bool m_isTruncated { false };
};
using RunList = Vector<Run, 10>;
const RunList& runs() const { return m_runs; }
@@ -581,6 +581,15 @@ LineBuilder::InlineItemRange LineBuilder::close(const InlineItemRange& needsLayo
lineEndsWithHyphen = lastTextContent && lastTextContent->needsHyphen;
}
m_successiveHyphenatedLineCount = lineEndsWithHyphen ? m_successiveHyphenatedLineCount + 1 : 0;

auto needsTextOverflowAdjustment = !isInIntrinsicWidthMode && rootStyle.textOverflow() == TextOverflow::Ellipsis && horizontalAvailableSpace < m_line.contentLogicalWidth();
if (needsTextOverflowAdjustment) {
static MainThreadNeverDestroyed<const AtomString> ellipsisStr(&horizontalEllipsis, 1);
auto ellipsisRun = WebCore::TextRun { ellipsisStr->string() };
auto ellipsisWidth = isFirstLine() ? root().firstLineStyle().fontCascade().width(ellipsisRun) : rootStyle.fontCascade().width(ellipsisRun);
auto logicalRightForContentWithoutEllipsis = std::max(0.f, horizontalAvailableSpace - ellipsisWidth);
m_line.truncate(logicalRightForContentWithoutEllipsis);
}
return lineRange;
}

@@ -41,12 +41,12 @@ struct Box {
struct Text {
WTF_MAKE_STRUCT_FAST_ALLOCATED;
public:
Text(size_t position, size_t length, const String& originalContent, String adjustedContentToRender = String(), bool hasHyphen = false);
Text(size_t position, size_t length, const String& originalContent, String adjustedContentToRender = String(), bool hasHyphen = false, std::optional<size_t> visuallyVisibleLength = std::nullopt);

size_t start() const { return m_start; }
size_t end() const { return start() + length(); }
size_t length() const { return m_length; }
std::optional<size_t> truncatedLength() const { return m_truncatedLength; }
std::optional<size_t> visuallyVisibleLength() const { return m_visuallyVisibleLength; }
StringView originalContent() const { return StringView(m_originalContent).substring(m_start, m_length); }
StringView renderedContent() const { return m_adjustedContentToRender.isNull() ? originalContent() : m_adjustedContentToRender; }

@@ -57,7 +57,7 @@ struct Box {

size_t m_start { 0 };
size_t m_length { 0 };
std::optional<size_t> m_truncatedLength { };
std::optional<size_t> m_visuallyVisibleLength { };
bool m_hasHyphen { false };
String m_originalContent;
String m_adjustedContentToRender;
@@ -79,7 +79,8 @@ struct Box {
First = 1 << 0,
Last = 1 << 1
};
Box(size_t lineIndex, Type, const Layout::Box&, UBiDiLevel, const FloatRect&, const FloatRect& inkOverflow, Expansion, std::optional<Text> = std::nullopt, bool hasContent = true, OptionSet<PositionWithinInlineLevelBox> = { });
enum class IsVisuallyHidden : uint8_t { Yes, No, Partially };
Box(size_t lineIndex, Type, const Layout::Box&, UBiDiLevel, const FloatRect&, const FloatRect& inkOverflow, Expansion, std::optional<Text> = std::nullopt, bool hasContent = true, IsVisuallyHidden isVisuallyHidden = IsVisuallyHidden::No, OptionSet<PositionWithinInlineLevelBox> = { });

bool isText() const { return m_type == Type::Text || isWordSeparator(); }
bool isWordSeparator() const { return m_type == Type::WordSeparator; }
@@ -101,6 +102,7 @@ struct Box {
bool isHorizontal() const { return style().isHorizontalWritingMode(); }

bool hasContent() const { return m_hasContent; }
IsVisuallyHidden isVisuallyHidden() const { return m_isVisuallyHidden; }

const FloatRect& visualRectIgnoringBlockDirection() const { return m_unflippedVisualRect; }
const FloatRect& inkOverflow() const { return m_inkOverflow; }
@@ -124,7 +126,6 @@ struct Box {
m_inkOverflow.move({ offset, { } });
}
void adjustInkOverflow(const FloatRect& childBorderBox) { return m_inkOverflow.uniteEvenIfEmpty(childBorderBox); }
void truncate(float truncatedWidth = 0.f, std::optional<size_t> truncatedLength = std::nullopt);
void setLeft(float pysicalLeft)
{
auto offset = pysicalLeft - left();
@@ -187,11 +188,12 @@ struct Box {
bool m_hasContent : 1;
bool m_isFirstForLayoutBox : 1;
bool m_isLastForLayoutBox : 1;
IsVisuallyHidden m_isVisuallyHidden { IsVisuallyHidden::No };
Expansion m_expansion;
std::optional<Text> m_text;
};

inline Box::Box(size_t lineIndex, Type type, const Layout::Box& layoutBox, UBiDiLevel bidiLevel, const FloatRect& physicalRect, const FloatRect& inkOverflow, Expansion expansion, std::optional<Text> text, bool hasContent, OptionSet<PositionWithinInlineLevelBox> positionWithinInlineLevelBox)
inline Box::Box(size_t lineIndex, Type type, const Layout::Box& layoutBox, UBiDiLevel bidiLevel, const FloatRect& physicalRect, const FloatRect& inkOverflow, Expansion expansion, std::optional<Text> text, bool hasContent, IsVisuallyHidden isVisuallyHidden, OptionSet<PositionWithinInlineLevelBox> positionWithinInlineLevelBox)
: m_lineIndex(lineIndex)
, m_type(type)
, m_layoutBox(layoutBox)
@@ -201,32 +203,22 @@ inline Box::Box(size_t lineIndex, Type type, const Layout::Box& layoutBox, UBiDi
, m_hasContent(hasContent)
, m_isFirstForLayoutBox(positionWithinInlineLevelBox.contains(PositionWithinInlineLevelBox::First))
, m_isLastForLayoutBox(positionWithinInlineLevelBox.contains(PositionWithinInlineLevelBox::Last))
, m_isVisuallyHidden(isVisuallyHidden)
, m_expansion(expansion)
, m_text(text)
{
}

inline Box::Text::Text(size_t start, size_t length, const String& originalContent, String adjustedContentToRender, bool hasHyphen)
inline Box::Text::Text(size_t start, size_t length, const String& originalContent, String adjustedContentToRender, bool hasHyphen, std::optional<size_t> visuallyVisibleLength)
: m_start(start)
, m_length(length)
, m_visuallyVisibleLength(visuallyVisibleLength)
, m_hasHyphen(hasHyphen)
, m_originalContent(originalContent)
, m_adjustedContentToRender(adjustedContentToRender)
{
}

inline void Box::truncate(float truncatedWidth, std::optional<size_t> truncatedLength)
{
if (m_text) {
ASSERT(!truncatedLength || m_text->m_length > *truncatedLength);
m_text->m_truncatedLength = truncatedLength.value_or(0);
m_text->m_hasHyphen = false;
}

m_unflippedVisualRect.setWidth(truncatedWidth);
m_inkOverflow.shiftMaxXEdgeTo(m_unflippedVisualRect.maxY());
}

}
}
#endif
@@ -104,7 +104,7 @@ DisplayBoxes InlineDisplayContentBuilder::build(const LineBuilder::LineContent&
processBidiContent(lineContent, lineBox, displayLine, boxes);
else
processNonBidiContent(lineContent, lineBox, displayLine, boxes);
processOverflownRunsForEllipsis(boxes, displayLine.right());
processOverflownRunsForEllipsis(lineContent, boxes, displayLine.right());
collectInkOverflowForTextDecorations(boxes, displayLine);
collectInkOverflowForInlineBoxes(boxes);
return boxes;
@@ -187,15 +187,17 @@ void InlineDisplayContentBuilder::appendTextDisplayBox(const Line::Run& lineRun,
auto adjustedContentToRender = [&] {
return text->needsHyphen ? makeString(StringView(content).substring(text->start, text->length), style.hyphenString()) : String();
};
auto isVisuallyHidden = !lineRun.isTruncated() ? InlineDisplay::Box::IsVisuallyHidden::No : !text->partiallyVisibleContent.has_value() ? InlineDisplay::Box::IsVisuallyHidden::Yes : InlineDisplay::Box::IsVisuallyHidden::Partially;
boxes.append({ m_lineIndex
, lineRun.isWordSeparator() ? InlineDisplay::Box::Type::WordSeparator : InlineDisplay::Box::Type::Text
, layoutBox
, lineRun.bidiLevel()
, textRunRect
, inkOverflow()
, lineRun.expansion()
, InlineDisplay::Box::Text { text->start, text->length, content, adjustedContentToRender(), text->needsHyphen }
, InlineDisplay::Box::Text { text->start, text->length, content, adjustedContentToRender(), text->needsHyphen, isVisuallyHidden == InlineDisplay::Box::IsVisuallyHidden::Partially ? std::make_optional(text->partiallyVisibleContent->length) : std::nullopt }
, true
, isVisuallyHidden
, { }
});
}
@@ -257,6 +259,8 @@ void InlineDisplayContentBuilder::appendAtomicInlineLevelDisplayBox(const Line::
, inkOverflow()
, lineRun.expansion()
, { }
, true
, lineRun.isTruncated() ? InlineDisplay::Box::IsVisuallyHidden::Yes : InlineDisplay::Box::IsVisuallyHidden::No
});
// Note that inline boxes are relative to the line and their top position can be negative.
// Atomic inline boxes are all set. Their margin/border/content box geometries are already computed. We just have to position them here.
@@ -308,6 +312,7 @@ void InlineDisplayContentBuilder::appendInlineBoxDisplayBox(const Line::Run& lin
, { }
, { }
, inlineBox.hasContent()
, lineRun.isTruncated() ? InlineDisplay::Box::IsVisuallyHidden::Yes : InlineDisplay::Box::IsVisuallyHidden::No
, isFirstLastBox(inlineBox)
});
// This inline box showed up first on this line.
@@ -335,6 +340,7 @@ void InlineDisplayContentBuilder::appendSpanningInlineBoxDisplayBox(const Line::
, { }
, { }
, inlineBox.hasContent()
, lineRun.isTruncated() ? InlineDisplay::Box::IsVisuallyHidden::Yes : InlineDisplay::Box::IsVisuallyHidden::No
, isFirstLastBox(inlineBox)
});
// Middle or end of the inline box. Let's stretch the box as needed.
@@ -750,7 +756,7 @@ void InlineDisplayContentBuilder::processBidiContent(const LineBuilder::LineCont
handleTrailingOpenInlineBoxes();
}

void InlineDisplayContentBuilder::processOverflownRunsForEllipsis(DisplayBoxes& boxes, InlineLayoutUnit lineBoxRight)
void InlineDisplayContentBuilder::processOverflownRunsForEllipsis(const LineBuilder::LineContent& lineContent, DisplayBoxes& boxes, InlineLayoutUnit lineBoxRight)
{
if (root().style().textOverflow() != TextOverflow::Ellipsis)
return;
@@ -759,51 +765,43 @@ void InlineDisplayContentBuilder::processOverflownRunsForEllipsis(DisplayBoxes&

if (rootInlineBox.right() <= lineBoxRight) {
ASSERT(boxes.last().right() <= lineBoxRight);
ASSERT(lineContent.runs.isEmpty() || !lineContent.runs[lineContent.runs.size() - 1].isTruncated());
return;
}

static MainThreadNeverDestroyed<const AtomString> ellipsisStr(&horizontalEllipsis, 1);
auto ellipsisRun = WebCore::TextRun { ellipsisStr->string() };
auto ellipsisWidth = !m_lineIndex ? root().firstLineStyle().fontCascade().width(ellipsisRun) : root().style().fontCascade().width(ellipsisRun);
auto firstTruncatedBoxIndex = boxes.size();

for (auto index = boxes.size(); index--;) {
auto& displayBox = boxes[index];

if (displayBox.left() >= lineBoxRight) {
// Fully overflown boxes are collapsed.
displayBox.truncate();
continue;
}

// We keep truncating content until after we can accommodate the ellipsis content
// 1. fully truncate in case of inline level boxes (ie non-text content) or if ellipsis content is wider than the overflowing one.
// 2. partially truncated to make room for the ellipsis box.
auto availableRoomForEllipsis = lineBoxRight - displayBox.left();
if (availableRoomForEllipsis <= ellipsisWidth) {
// Can't accommodate the ellipsis content here. We need to truncate non-overflowing boxes too.
displayBox.truncate();
continue;
auto ellipsisVisualLeft = [&] {
auto truncatedWidth = [&] () -> float {
for (auto& lineRun : lineContent.runs) {
if (lineRun.isTruncated())
return lineRun.isText() ? lineRun.textContent()->partiallyVisibleContent->width : 0.f;
}
return { };
};
auto left = 0.f;
for (auto& box : boxes) {
if (box.isInlineBox())
continue;
switch (box.isVisuallyHidden()) {
case InlineDisplay::Box::IsVisuallyHidden::No:
left = std::max(left, box.right());
break;
case InlineDisplay::Box::IsVisuallyHidden::Partially:
return std::max(left, box.left() + truncatedWidth());
case InlineDisplay::Box::IsVisuallyHidden::Yes:
return left;
default:
ASSERT_NOT_REACHED();
break;
}
}
return left;
}();
auto ellispisBoxRect = InlineRect { rootInlineBox.top(), ellipsisVisualLeft, ellipsisWidth, rootInlineBox.height() };

if (displayBox.isText()) {
auto text = *displayBox.text();
// FIXME: Check if it needs adjustment for RTL direction.
auto truncatedContent = TextUtil::breakWord(downcast<InlineTextBox>(displayBox.layoutBox()), text.start(), text.length(), displayBox.width(), availableRoomForEllipsis - ellipsisWidth, { }, displayBox.style().fontCascade());
displayBox.truncate(truncatedContent.logicalWidth, truncatedContent.length);
} else
displayBox.truncate();

firstTruncatedBoxIndex = index;
break;
}
ASSERT(firstTruncatedBoxIndex < boxes.size());
// Collapse truncated runs.
auto contentRight = boxes[firstTruncatedBoxIndex].right();
for (auto index = firstTruncatedBoxIndex + 1; index < boxes.size(); ++index)
boxes[index].moveHorizontally(contentRight - boxes[index].left());
// And append the ellipsis box as the trailing item.
auto ellispisBoxRect = InlineRect { rootInlineBox.top(), contentRight, ellipsisWidth, rootInlineBox.height() };
boxes.append({ m_lineIndex
, InlineDisplay::Box::Type::Ellipsis
, root()
@@ -54,7 +54,7 @@ class InlineDisplayContentBuilder {
private:
void processNonBidiContent(const LineBuilder::LineContent&, const LineBox&, const InlineDisplay::Line&, DisplayBoxes&);
void processBidiContent(const LineBuilder::LineContent&, const LineBox&, const InlineDisplay::Line&, DisplayBoxes&);
void processOverflownRunsForEllipsis(DisplayBoxes&, InlineLayoutUnit lineBoxRight);
void processOverflownRunsForEllipsis(const LineBuilder::LineContent&, DisplayBoxes&, InlineLayoutUnit lineBoxRight);
void collectInkOverflowForInlineBoxes(DisplayBoxes&);
void collectInkOverflowForTextDecorations(DisplayBoxes&, const InlineDisplay::Line&);

@@ -76,7 +76,7 @@ class BoxModernPath {
length(),
box().text()->hasHyphen() ? box().style().hyphenString().length() : 0,
box().isLineBreak(),
box().text()->truncatedLength()
box().text()->visuallyVisibleLength()
};
}

0 comments on commit dfaf825

Please sign in to comment.