Using non-breaking spaces as a hint to break too long words

Change-Id: Ic88e961ea3fb08e61340895115f514c6f5dec163
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/379798
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Julia Lavrova <jlavrova@google.com>
This commit is contained in:
Julia Lavrova 2021-03-04 16:29:06 -05:00 committed by Skia Commit-Bot
parent 07ee548d5b
commit c70f8c331d
9 changed files with 57 additions and 23 deletions

View File

@ -3416,20 +3416,6 @@ private:
using INHERITED = Sample; using INHERITED = Sample;
}; };
void print_line_metrics(std::vector<LineMetrics> metrics) {
for (size_t i = 0; i < metrics.size(); ++i) {
auto& line = metrics[i];
printf("Line[%zd] startIndex=%zd, endIndex=%zd, endExcludingWhitespaces=%zd, _endIncludingNewline=%zd, hardBreak=%d, baseline=%.2f, ascent=%.2f, descent=%.2f, unscaledAscent=%.2f, height=%.2f, width=%.2f, left=%.2f, lineNumber=%zd\n",
i,
line.fStartIndex, line.fEndIndex,
line.fEndExcludingWhitespaces, line.fEndIncludingNewline,
line.fHardBreak,
line.fBaseline, line.fAscent, line.fDescent, line.fUnscaledAscent,
line.fHeight, line.fWidth, line.fLeft,
line.fLineNumber);
}
}
class ParagraphView58 : public ParagraphView_Base { class ParagraphView58 : public ParagraphView_Base {
protected: protected:
SkString name() override { return SkString("Paragraph58"); } SkString name() override { return SkString("Paragraph58"); }
@ -3446,21 +3432,14 @@ protected:
ParagraphBuilderImpl builder(paragraph_style, fontCollection); ParagraphBuilderImpl builder(paragraph_style, fontCollection);
TextStyle text_style; TextStyle text_style;
text_style.setFontFamilies({SkString("Roboto")}); text_style.setFontFamilies({SkString("Roboto")});
text_style.setFontSize(32); text_style.setFontSize(40);
text_style.setHeight(5.0);
text_style.setHeightOverride(true);
text_style.setColor(SK_ColorBLACK); text_style.setColor(SK_ColorBLACK);
builder.pushStyle(text_style); builder.pushStyle(text_style);
builder.addText("Line1\nLine2\nLine3\nLine4\n"); builder.addText(u"Text1 Google\u00A0Pay Text2");
auto paragraph = builder.Build(); auto paragraph = builder.Build();
paragraph->layout(width()); paragraph->layout(width());
paragraph->paint(canvas, 0, 0); paragraph->paint(canvas, 0, 0);
std::vector<LineMetrics> metrics;
paragraph->getLineMetrics(metrics);
print_line_metrics(metrics);
} }
private: private:

View File

@ -28,6 +28,11 @@ namespace textlayout {
namespace { namespace {
static inline SkUnichar nextUtf8Unit(const char** ptr, const char* end) {
SkUnichar val = SkUTF::NextUTF8(ptr, end);
return val < 0 ? 0xFFFD : val;
}
SkScalar littleRound(SkScalar a) { SkScalar littleRound(SkScalar a) {
// This rounding is done to match Flutter tests. Must be removed.. // This rounding is done to match Flutter tests. Must be removed..
auto val = std::fabs(a); auto val = std::fabs(a);
@ -736,6 +741,18 @@ size_t ParagraphImpl::getWhitespacesLength(TextRange textRange) {
return len; return len;
} }
bool ParagraphImpl::isSpace(TextRange textRange) {
auto text = ParagraphImpl::text(textRange);
const char* ch = text.begin();
while (ch != text.end()) {
SkUnichar unicode = nextUtf8Unit(&ch, text.end());
if (!fUnicode->isSpace(unicode)) {
return false;
}
}
return true;
}
void ParagraphImpl::getLineMetrics(std::vector<LineMetrics>& metrics) { void ParagraphImpl::getLineMetrics(std::vector<LineMetrics>& metrics) {
metrics.clear(); metrics.clear();
for (auto& line : fLines) { for (auto& line : fLines) {

View File

@ -220,6 +220,7 @@ public:
using CodeUnitRangeVisitor = std::function<bool(TextRange textRange)>; using CodeUnitRangeVisitor = std::function<bool(TextRange textRange)>;
void forEachCodeUnitPropertyRange(CodeUnitFlags property, CodeUnitRangeVisitor visitor); void forEachCodeUnitPropertyRange(CodeUnitFlags property, CodeUnitRangeVisitor visitor);
size_t getWhitespacesLength(TextRange textRange); size_t getWhitespacesLength(TextRange textRange);
bool isSpace(TextRange textRange);
bool codeUnitHasProperty(size_t index, CodeUnitFlags property) const { return (fCodeUnitProperties[index] & property) == property; } bool codeUnitHasProperty(size_t index, CodeUnitFlags property) const { return (fCodeUnitProperties[index] & property) == property; }

View File

@ -383,6 +383,7 @@ Cluster::Cluster(ParagraphImpl* owner,
, fHalfLetterSpacing(0.0) { , fHalfLetterSpacing(0.0) {
size_t len = fOwner->getWhitespacesLength(fTextRange); size_t len = fOwner->getWhitespacesLength(fTextRange);
fIsWhiteSpaces = (len == this->fTextRange.width()); fIsWhiteSpaces = (len == this->fTextRange.width());
fIsSpaces = fOwner->isSpace(fTextRange);
} }
} // namespace textlayout } // namespace textlayout

View File

@ -248,6 +248,7 @@ public:
} }
bool isWhitespaces() const { return fIsWhiteSpaces; } bool isWhitespaces() const { return fIsWhiteSpaces; }
bool isSpaces() const { return fIsSpaces; }
bool isHardBreak() const; bool isHardBreak() const;
bool isSoftBreak() const; bool isSoftBreak() const;
bool isGraphemeBreak() const; bool isGraphemeBreak() const;
@ -297,6 +298,7 @@ private:
SkScalar fHeight; SkScalar fHeight;
SkScalar fHalfLetterSpacing; SkScalar fHalfLetterSpacing;
bool fIsWhiteSpaces; bool fIsWhiteSpaces;
bool fIsSpaces;
}; };
class InternalLineMetrics { class InternalLineMetrics {

View File

@ -29,6 +29,7 @@ void TextWrapper::lookAhead(SkScalar maxWidth, Cluster* endOfClusters) {
fClusters.startFrom(fEndLine.startCluster(), fEndLine.startPos()); fClusters.startFrom(fEndLine.startCluster(), fEndLine.startPos());
fClip.startFrom(fEndLine.startCluster(), fEndLine.startPos()); fClip.startFrom(fEndLine.startCluster(), fEndLine.startPos());
Cluster* nextNonBreakingSpace = nullptr;
for (auto cluster = fEndLine.endCluster(); cluster < endOfClusters; ++cluster) { for (auto cluster = fEndLine.endCluster(); cluster < endOfClusters; ++cluster) {
// TODO: Trying to deal with flutter rounding problem. Must be removed... // TODO: Trying to deal with flutter rounding problem. Must be removed...
auto width = fWords.width() + fClusters.width() + cluster->width(); auto width = fWords.width() + fClusters.width() + cluster->width();
@ -61,6 +62,7 @@ void TextWrapper::lookAhead(SkScalar maxWidth, Cluster* endOfClusters) {
// Walk further to see if there is a too long word, cluster or glyph // Walk further to see if there is a too long word, cluster or glyph
SkScalar nextWordLength = fClusters.width(); SkScalar nextWordLength = fClusters.width();
SkScalar nextShortWordLength = nextWordLength;
for (auto further = cluster; further != endOfClusters; ++further) { for (auto further = cluster; further != endOfClusters; ++further) {
if (further->isSoftBreak() || further->isHardBreak() || further->isWhitespaces()) { if (further->isSoftBreak() || further->isHardBreak() || further->isWhitespaces()) {
break; break;
@ -69,6 +71,13 @@ void TextWrapper::lookAhead(SkScalar maxWidth, Cluster* endOfClusters) {
// Placeholder ends the word // Placeholder ends the word
break; break;
} }
if (further->isSpaces() && nextWordLength <= maxWidth) {
// The cluster is spaces but not the end of the word in a normal sense
nextNonBreakingSpace = further;
nextShortWordLength = nextWordLength;
}
if (maxWidth == 0) { if (maxWidth == 0) {
// This is a tricky flutter case: layout(width:0) places 1 cluster on each line // This is a tricky flutter case: layout(width:0) places 1 cluster on each line
nextWordLength = std::max(nextWordLength, further->width()); nextWordLength = std::max(nextWordLength, further->width());
@ -77,6 +86,22 @@ void TextWrapper::lookAhead(SkScalar maxWidth, Cluster* endOfClusters) {
} }
} }
if (nextWordLength > maxWidth) { if (nextWordLength > maxWidth) {
if (nextNonBreakingSpace != nullptr) {
// We only get here if the non-breaking space improves our situation
// (allows us to break the text to fit the word)
auto shortLength = littleRound(fWords.width() + nextShortWordLength);
if (shortLength <= maxWidth) {
// We can add the short word to the existing line
fClusters = TextStretch(fClusters.startCluster(), nextNonBreakingSpace, fClusters.metrics().getForceStrut());
fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, nextShortWordLength);
fWords.extend(fClusters);
} else {
// We can place the short word on the next line
fClusters.clean();
}
// Either way we are not in "word is too long" situation anymore
break;
}
// If the word is too long we can break it right now and hope it's enough // If the word is too long we can break it right now and hope it's enough
fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, nextWordLength); fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, nextWordLength);
if (fClusters.endPos() - fClusters.startPos() > 1 || if (fClusters.endPos() - fClusters.startPos() > 1 ||

View File

@ -41,7 +41,11 @@ class TextWrapper {
if (c->run() != nullptr) { if (c->run() != nullptr) {
fMetrics.add(c->run()); fMetrics.add(c->run());
} }
if (c < e) {
fWidth += c->width();
}
} }
fWidthWithGhostSpaces = fWidth;
} }
inline SkScalar width() const { return fWidth; } inline SkScalar width() const { return fWidth; }

View File

@ -113,6 +113,7 @@ class SKUNICODE_API SkUnicode {
virtual bool isControl(SkUnichar utf8) = 0; virtual bool isControl(SkUnichar utf8) = 0;
virtual bool isWhitespace(SkUnichar utf8) = 0; virtual bool isWhitespace(SkUnichar utf8) = 0;
virtual bool isSpace(SkUnichar utf8) = 0;
virtual SkString convertUtf16ToUtf8(const std::u16string& utf16) = 0; virtual SkString convertUtf16ToUtf8(const std::u16string& utf16) = 0;
// Methods used in SkShaper // Methods used in SkShaper

View File

@ -459,6 +459,10 @@ public:
return u_isWhitespace(utf8); return u_isWhitespace(utf8);
} }
bool isSpace(SkUnichar utf8) override {
return u_isspace(utf8);
}
static bool isHardLineBreak(SkUnichar utf8) { static bool isHardLineBreak(SkUnichar utf8) {
auto property = u_getIntPropertyValue(utf8, UCHAR_LINE_BREAK); auto property = u_getIntPropertyValue(utf8, UCHAR_LINE_BREAK);
return property == U_LB_LINE_FEED || property == U_LB_MANDATORY_BREAK; return property == U_LB_LINE_FEED || property == U_LB_MANDATORY_BREAK;