+2018-07-23 Zalan Bujtas <zalan@apple.com>
+
+ [LCF][IFC] Add support for hyphenation.
+ https://bugs.webkit.org/show_bug.cgi?id=187913
+
+ Reviewed by Antti Koivisto.
+
+ Move the hyphenation logic over from SimpleLineLayout::TextFragmentIterator.
+
+ * layout/inlineformatting/textlayout/Runs.h:
+ (WebCore::Layout::TextRun::hasHyphen const):
+ (WebCore::Layout::LayoutRun::setHasHyphen):
+ (WebCore::Layout::LayoutRun::hasHyphen const):
+ (WebCore::Layout::LayoutRun::LayoutRun):
+ (WebCore::Layout::TextRun::createNonWhitespaceRunWithHyphen):
+ (WebCore::Layout::TextRun::TextRun):
+ * layout/inlineformatting/textlayout/TextContentProvider.cpp:
+ (WebCore::Layout::TextContentProvider::findTextItemSlow const):
+ (WebCore::Layout::TextContentProvider::width const):
+ (WebCore::Layout::TextContentProvider::hyphenPositionBefore const):
+ * layout/inlineformatting/textlayout/TextContentProvider.h:
+ * layout/inlineformatting/textlayout/simple/SimpleLineBreaker.cpp:
+ (WebCore::Layout::SimpleLineBreaker::Line::setTextAlign):
+ (WebCore::Layout::SimpleLineBreaker::Line::justifyRuns):
+ (WebCore::Layout::SimpleLineBreaker::Line::adjustRunsForTextAlign):
+ (WebCore::Layout::SimpleLineBreaker::Line::collectExpansionOpportunities):
+ (WebCore::Layout::SimpleLineBreaker::Line::closeLastRun):
+ (WebCore::Layout::SimpleLineBreaker::Line::append):
+ (WebCore::Layout::SimpleLineBreaker::Line::reset):
+ (WebCore::Layout::SimpleLineBreaker::Style::Style):
+ (WebCore::Layout::SimpleLineBreaker::handleLineEnd):
+ (WebCore::Layout::SimpleLineBreaker::createRunsForLine):
+ (WebCore::Layout::SimpleLineBreaker::hyphenPositionBefore const):
+ (WebCore::Layout::SimpleLineBreaker::adjustSplitPositionWithHyphenation const):
+ (WebCore::Layout::SimpleLineBreaker::split const):
+ * layout/inlineformatting/textlayout/simple/SimpleLineBreaker.h:
+
2018-07-23 Antoine Quint <graouts@apple.com>
[Web Animations] Querying the current time of a finished CSSAnimation after removing its target leads to a crash
};
static TextRun createWhitespaceRun(ContentPosition start, ContentPosition end, float width, bool isCollapsed);
static TextRun createNonWhitespaceRun(ContentPosition start, ContentPosition end, float width);
+ static TextRun createNonWhitespaceRunWithHyphen(ContentPosition start, ContentPosition end, float width);
static TextRun createSoftLineBreakRun(ContentPosition);
static TextRun createHardLineBreakRun(ContentPosition);
TextRun() = default;
- TextRun(ContentPosition start, ContentPosition end, Type, float width = 0, bool isCollapsed = false);
+ TextRun(ContentPosition start, ContentPosition end, Type, float width = 0, bool isCollapsed = false, bool hasHyphen = false);
ContentPosition start() const;
ContentPosition end() const;
Type type() const { return m_type; }
void setIsCollapsed(bool isCollapsed) { m_isCollapsed = isCollapsed; }
+ bool hasHyphen() const { return m_hasHyphen; }
void setWidth(float width) { m_width = width; }
private:
Type m_type { Type::Invalid };
float m_width { 0 };
bool m_isCollapsed { false };
+ bool m_hasHyphen { false };
};
struct LayoutRun {
public:
- LayoutRun(ContentPosition start, ContentPosition end, float left, float right);
+ LayoutRun(ContentPosition start, ContentPosition end, float left, float right, bool hasHyphen);
ContentPosition start() const { return m_start; }
ContentPosition end() const { return m_end; }
void setIsEndOfLine() { m_isEndOfLine = true; }
void setExpansion(ExpansionBehavior, float expansion);
+ void setHasHyphen() { m_hasHyphen = true; }
+ bool hasHyphen() const { return m_hasHyphen; }
private:
ContentPosition m_start { 0 };
float m_left { 0 };
float m_right { 0 };
bool m_isEndOfLine { false };
+ bool m_hasHyphen { false };
float m_expansion { 0 };
ExpansionBehavior m_expansionBehavior { ForbidLeadingExpansion | ForbidTrailingExpansion };
};
return *this;
}
-inline LayoutRun::LayoutRun(ContentPosition start, ContentPosition end, float left, float right)
+inline LayoutRun::LayoutRun(ContentPosition start, ContentPosition end, float left, float right, bool hasHyphen)
: m_start(start)
, m_end(end)
, m_left(left)
, m_right(right)
+ , m_hasHyphen(hasHyphen)
{
}
return { start, end, Type::NonWhitespace, width };
}
+inline TextRun TextRun::createNonWhitespaceRunWithHyphen(ContentPosition start, ContentPosition end, float width)
+{
+ return { start, end, Type::NonWhitespace, width, false, true };
+}
+
inline TextRun TextRun::createSoftLineBreakRun(ContentPosition position)
{
return { position, position + 1, Type::SoftLineBreak };
return { position, position, Type::HardLineBreak };
}
-inline TextRun::TextRun(ContentPosition start, ContentPosition end, Type type, float width, bool isCollapsed)
+inline TextRun::TextRun(ContentPosition start, ContentPosition end, Type type, float width, bool isCollapsed, bool hasHyphen)
: m_start(start)
, m_end(end)
, m_type(type)
, m_width(width)
, m_isCollapsed(isCollapsed)
+ , m_hasHyphen(hasHyphen)
{
}
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
#include "FontCascade.h"
+#include "Hyphenation.h"
#include "RenderStyle.h"
#include "SimpleTextRunGenerator.h"
#include <wtf/IsoMallocInlines.h>
return textItem.start <= position && position < textItem.end;
}
-float TextContentProvider::width(ContentPosition from, ContentPosition to, float xPosition) const
+const TextContentProvider::TextItem* TextContentProvider::findTextItemSlow(ContentPosition position) const
{
- auto textItemPositionSlow = [&]() -> unsigned {
- // Since this is an iterator like class, check the next item first (instead of starting the search from the beginning).
- if (auto* textItem = (++m_textContentIterator).current()) {
- if (contains(from, *textItem))
- return textItem->start;
- }
+ // Since this is an iterator like class, check the next item first (instead of starting the search from the beginning).
+ if (auto* textItem = (++m_textContentIterator).current()) {
+ if (contains(position, *textItem))
+ return textItem;
+ }
- m_textContentIterator.reset();
- while (auto* textItem = m_textContentIterator.current()) {
- if (contains(from, *textItem))
- return textItem->start;
- ++m_textContentIterator;
- }
+ m_textContentIterator.reset();
+ while (auto* textItem = m_textContentIterator.current()) {
+ if (contains(position, *textItem))
+ return textItem;
+ ++m_textContentIterator;
+ }
- ASSERT_NOT_REACHED();
- return 0;
- };
+ ASSERT_NOT_REACHED();
+ return nullptr;
+}
+float TextContentProvider::width(ContentPosition from, ContentPosition to, float xPosition) const
+{
if (from >= to) {
ASSERT_NOT_REACHED();
return 0;
float width = 0;
auto length = to - from;
auto* textItem = m_textContentIterator.current();
- auto startPosition = from - (textItem && contains(from, *textItem) ? textItem->start : textItemPositionSlow());
+ auto startPosition = from - (textItem && contains(from, *textItem) ? textItem->start : findTextItemSlow(from)->start);
while (length) {
textItem = m_textContentIterator.current();
return width;
}
+std::optional<ContentPosition> TextContentProvider::hyphenPositionBefore(ContentPosition from, ContentPosition to, ContentPosition before) const
+{
+ auto contentLength = length();
+ if (before >= contentLength || before < from || before > to) {
+ ASSERT_NOT_REACHED();
+ return { };
+ }
+
+ auto* textItem = m_textContentIterator.current();
+ if (!textItem || !contains(before, *textItem))
+ textItem = findTextItemSlow(before);
+
+ auto fromItemPosition = from - textItem->start;
+ auto stringForHyphenLocation = StringView(textItem->text).substring(fromItemPosition, to - from);
+
+ // adjustedBefore -> ContentPosition -> ItemPosition -> run position.
+ auto adjustedBefore = before - from;
+ auto hyphenLocation = lastHyphenLocation(stringForHyphenLocation, adjustedBefore, textItem->style.locale);
+ if (!hyphenLocation)
+ return { };
+
+ return from + hyphenLocation;
+}
+
unsigned TextContentProvider::length() const
{
if (!m_textContent.size())
unsigned length() const;
float width(ContentPosition from, ContentPosition to, float xPosition) const;
+ std::optional<ContentPosition> hyphenPositionBefore(ContentPosition from, ContentPosition to, ContentPosition before) const;
class Iterator {
public:
private:
friend class Iterator;
+ const TextItem* findTextItemSlow(ContentPosition) const;
float textWidth(const TextItem&, ItemPosition from, ItemPosition to, float xPosition) const;
float fixedPitchWidth(String, const TextItem::Style&, ItemPosition from, ItemPosition to, float xPosition) const;
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
#include "FontCascade.h"
+#include "Hyphenation.h"
#include "InlineFormattingContext.h"
#include "LayoutContext.h"
#include "RenderStyle.h"
void SimpleLineBreaker::Line::setTextAlign(TextAlignMode textAlign)
{
m_style.textAlign = textAlign;
- m_collectExpansionOpportunities = textAlign == TextAlignMode::Justify;
+ m_collectExpansionOpportunities = textAlign == TextAlignMode::Justify;
}
float SimpleLineBreaker::Line::adjustedLeftForTextAlign(TextAlignMode textAlign) const
return;
}
auto& layoutRun = m_layoutRuns.at(runIndex++);
- auto expansionForRun = expansionEntry.count * expansion;
+ auto expansionForRun = expansionEntry.count * expansion;
layoutRun.setExpansion(expansionEntry.behavior, expansionForRun);
layoutRun.setLeft(layoutRun.left() + accumulatedExpansion);
justifyRuns();
return;
}
+
auto adjustedLeft = adjustedLeftForTextAlign(textAlign);
if (adjustedLeft == m_left)
return;
// Create an entry for this new layout run.
m_expansionOpportunityList.append({ });
}
-
+
if (!textRun.length())
return;
++m_expansionOpportunityList.last().count;
if (textRun.isNonWhitespace())
- m_lastNonWhitespaceExpansionOppportunity = m_expansionOpportunityList.last();
+ m_lastNonWhitespaceExpansionOppportunity = m_expansionOpportunityList.last();
}
void SimpleLineBreaker::Line::closeLastRun()
return;
m_layoutRuns.last().setIsEndOfLine();
-
+
// Forbid trailing expansion for the last run on line.
if (!m_collectExpansionOpportunities || m_expansionOpportunityList.isEmpty())
return;
-
- auto& lastExpansionEntry = m_expansionOpportunityList.last();
+
+ auto& lastExpansionEntry = m_expansionOpportunityList.last();
auto expansionBehavior = lastExpansionEntry.behavior;
// Remove allow and add forbid.
expansionBehavior ^= AllowTrailingExpansion;
// New line needs new run.
if (textRunCreatesNewLayoutRun)
- m_layoutRuns.append({ start, end, previousLogicalRight, previousLogicalRight + textRun.width() });
+ m_layoutRuns.append({ start, end, previousLogicalRight, previousLogicalRight + textRun.width(), textRun.hasHyphen() });
else {
auto& lastRun = m_layoutRuns.last();
lastRun.setEnd(end);
lastRun.setRight(lastRun.right() + textRun.width());
+ if (textRun.hasHyphen())
+ lastRun.setHasHyphen();
}
m_lastTextRun = textRun;
void SimpleLineBreaker::Line::reset()
{
m_runsWidth = 0;
- m_firstRunIndex = m_layoutRuns.size();
+ m_firstRunIndex = m_layoutRuns.size();
m_availableWidth = 0;
m_trailingWhitespaceWidth = 0;
m_expansionOpportunityList.clear();
// FIXME: Use variable style based on the current text run.
SimpleLineBreaker::Style::Style(const RenderStyle& style)
- : wrapLines(style.autoWrap())
+ : font(style.fontCascade())
+ , wrapLines(style.autoWrap())
, breakAnyWordOnOverflow(style.wordBreak() == WordBreak::BreakAll && wrapLines)
, breakFirstWordOnOverflow(breakAnyWordOnOverflow || (style.breakWords() && (wrapLines || style.preserveNewline())))
, collapseWhitespace(style.collapseWhiteSpace())
, preWrap(wrapLines && !collapseWhitespace)
, preserveNewline(style.preserveNewline())
, textAlign(style.textAlign())
+ , shouldHyphenate(style.hyphens() == Hyphens::Auto && canHyphenate(style.locale()))
+ , hyphenStringWidth(shouldHyphenate ? font.width(WebCore::TextRun(String(style.hyphenString()))) : 0)
+ , hyphenLimitBefore(style.hyphenationLimitBefore() < 0 ? 2 : style.hyphenationLimitBefore())
+ , hyphenLimitAfter(style.hyphenationLimitAfter() < 0 ? 2 : style.hyphenationLimitAfter())
+ , locale(style.locale())
{
+ if (style.hyphenationLimitLines() > -1)
+ hyphenLimitLines = style.hyphenationLimitLines();
}
SimpleLineBreaker::SimpleLineBreaker(const Vector<TextRun>& textRuns, const TextContentProvider& contentProvider, LineConstraintList&& lineConstraintList, const RenderStyle& style)
void SimpleLineBreaker::handleLineEnd()
{
- auto lineHasContent = m_currentLine.hasContent();
+ auto lineHasContent = m_currentLine.hasContent();
if (lineHasContent) {
ASSERT(m_layoutRuns.size());
++m_numberOfLines;
m_currentLine.closeLastRun();
- auto lastLine = !m_textRunList.current();
+ auto lastLine = !m_textRunList.current();
m_currentLine.adjustRunsForTextAlign(lastLine);
}
+ // Check if we need to disable hyphenation.
+ if (m_style.hyphenLimitLines) {
+ if (!lineHasContent || (m_layoutRuns.size() && !m_layoutRuns.last().hasHyphen()))
+ m_numberOfPrecedingLinesWithHyphen = 0;
+ else
+ ++m_numberOfPrecedingLinesWithHyphen;
+ m_hyphenationIsDisabled = m_numberOfPrecedingLinesWithHyphen >= *m_style.hyphenLimitLines;
+ }
+
m_previousLineHasNonForcedContent = lineHasContent && m_currentLine.availableWidth() >= 0;
m_currentLine.reset();
}
}
ASSERT(textRun->isNonWhitespace());
+ // Find out if this non-whitespace fragment has a hyphen where we can break.
+ if (m_style.shouldHyphenate && !m_hyphenationIsDisabled) {
+ if (!splitTextRun(*textRun)) {
+ ++m_textRunList;
+ break;
+ }
+ }
+
// Non-breakable non-whitespace first run. Add it to the current line. -it overflows though.
if (!m_currentLine.hasContent()) {
handleOverflownRun();
m_currentLine.collapseTrailingWhitespace();
}
+std::optional<ContentPosition> SimpleLineBreaker::hyphenPositionBefore(const TextRun& textRun, ContentPosition before) const
+{
+ // Enough characters before the split position?
+ if (before <= textRun.start() + m_style.hyphenLimitBefore)
+ return { };
+
+ // Adjust before to accommodate the limit-after value (this is the last potential hyphen location).
+ before = std::min(before, textRun.end() - m_style.hyphenLimitAfter + 1);
+
+ auto hyphenLocation = m_contentProvider.hyphenPositionBefore(textRun.start(), textRun.end(), before);
+ if (!hyphenLocation)
+ return { };
+
+ ASSERT(hyphenLocation >= textRun.start() && hyphenLocation <= textRun.end());
+ // Check if there are enough characters before and after the hyphen.
+ if (*hyphenLocation < textRun.start() + m_style.hyphenLimitBefore || m_style.hyphenLimitAfter > (textRun.end() - *hyphenLocation))
+ return { };
+
+ return hyphenLocation;
+}
+
+std::optional<ContentPosition> SimpleLineBreaker::adjustSplitPositionWithHyphenation(const TextRun& textRun, ContentPosition splitPosition, float leftSideWidth) const
+{
+ ASSERT(textRun.isNonWhitespace());
+
+ // Use hyphen?
+ if (!m_style.shouldHyphenate || m_hyphenationIsDisabled)
+ return { };
+
+ // Check if there are enough characters in the run.
+ auto runLength = textRun.length();
+ if (m_style.hyphenLimitBefore >= runLength || m_style.hyphenLimitAfter >= runLength || m_style.hyphenLimitBefore + m_style.hyphenLimitAfter > runLength)
+ return { };
+
+ // FIXME: This is a workaround for webkit.org/b/169613. See maxPrefixWidth computation in tryHyphenating().
+ // It does not work properly with non-collapsed leading tabs when font is enlarged.
+ auto adjustedAvailableWidth = m_currentLine.availableWidth() - m_style.hyphenStringWidth;
+ if (m_currentLine.hasContent())
+ adjustedAvailableWidth += m_style.font.spaceWidth();
+
+ if (!enoughWidthForHyphenation(adjustedAvailableWidth, m_style.font.pixelSize()))
+ return { };
+
+ // Find the split position where hyphen surely fits (we might be able to fit the hyphen at the split position).
+ auto left = textRun.start();
+ auto right = splitPosition;
+ while (leftSideWidth + m_style.hyphenStringWidth > m_currentLine.availableWidth()) {
+ if (--right <= left)
+ return { }; // No space for hyphen.
+ // FIXME: for correctness (kerning) we should instead measure the left side.
+ leftSideWidth -= m_contentProvider.width(right, right + 1, 0);
+ }
+
+ // Find out if there's an actual hyphen opportinity at this position (or before).
+ return hyphenPositionBefore(textRun, right + 1);
+}
+
bool SimpleLineBreaker::splitTextRun(const TextRun& textRun)
{
// Single character handling.
}
}
- // Keep at least one character on empty lines.f
+ // Keep at least one character on empty lines.
+ bool splitHasHypen = false;
left = textRun.start();
if (left >= right && !m_currentLine.hasContent()) {
right = left + 1;
- leftSideWidth = m_contentProvider.width(textRun.start(), right, 0);
+ leftSideWidth = m_contentProvider.width(left, right, 0);
+ } else if (textRun.isNonWhitespace()) {
+ if (auto hyphenPosition = adjustSplitPositionWithHyphenation(textRun, right, leftSideWidth)) {
+ splitHasHypen = true;
+ right = *hyphenPosition;
+ leftSideWidth = m_contentProvider.width(left, right, 0) + m_style.hyphenStringWidth;
+ }
}
auto rightSideWidth = m_contentProvider.width(right, textRun.end(), 0);
if (textRun.isNonWhitespace())
- return { TextRun::createNonWhitespaceRun(left, right, leftSideWidth), TextRun::createNonWhitespaceRun(right, textRun.end(), rightSideWidth) };
+ return { splitHasHypen ? TextRun::createNonWhitespaceRunWithHyphen(left, right, leftSideWidth) : TextRun::createNonWhitespaceRun(left, right, leftSideWidth),
+ TextRun::createNonWhitespaceRun(right, textRun.end(), rightSideWidth) };
// We never split collapsed whitespace.
ASSERT(textRun.isWhitespace());
namespace WebCore {
+class FontCascade;
class RenderStyle;
namespace Layout {
struct Style {
explicit Style(const RenderStyle&);
+ const FontCascade& font;
bool wrapLines { false };
bool breakAnyWordOnOverflow { false };
bool breakFirstWordOnOverflow { false };
bool preWrap { false };
bool preserveNewline { false };
TextAlignMode textAlign { TextAlignMode::Left };
+ bool shouldHyphenate;
+ float hyphenStringWidth;
+ ItemPosition hyphenLimitBefore;
+ ItemPosition hyphenLimitAfter;
+ AtomicString locale;
+ std::optional<unsigned> hyphenLimitLines;
};
class TextRunList {
bool collapseWhitespace { false };
TextAlignMode textAlign { TextAlignMode::Left };
};
+
float adjustedLeftForTextAlign(TextAlignMode) const;
void justifyRuns();
void collectExpansionOpportunities(const TextRun&, bool textRunCreatesNewLayoutRun);
Vector<LayoutRun>& m_layoutRuns;
Style m_style;
-
+
float m_runsWidth { 0 };
float m_availableWidth { 0 };
float m_left { 0 };
void collapseTrailingWhitespace();
bool splitTextRun(const TextRun&);
TextRunSplitPair split(const TextRun&, float leftSideMaximumWidth) const;
+ std::optional<ContentPosition> hyphenPositionBefore(const TextRun&, ContentPosition before) const;
+ std::optional<ContentPosition> adjustSplitPositionWithHyphenation(const TextRun&, ContentPosition splitPosition, float leftSideWidth) const;
LineConstraint lineConstraint(float verticalPosition);
float verticalPosition() const { return m_numberOfLines * m_lineHeight; }
unsigned m_numberOfLines { 0 };
bool m_previousLineHasNonForcedContent { false };
float m_lineHeight { 0 };
+ bool m_hyphenationIsDisabled { false };
+ unsigned m_numberOfPrecedingLinesWithHyphen { 0 };
};
inline std::optional<TextRun> SimpleLineBreaker::TextRunList::current() const