| // Copyright 2019 Google LLC. |
| #include "modules/skparagraph/src/ParagraphImpl.h" |
| #include "modules/skparagraph/src/TextWrapper.h" |
| |
| namespace skia { |
| namespace textlayout { |
| |
| namespace { |
| SkScalar littleRound(SkScalar a) { |
| // This rounding is done to match Flutter tests. Must be removed.. |
| auto val = std::fabs(a); |
| if (val < 10000) { |
| return SkScalarRoundToScalar(a * 100) * (1.0f/100); |
| } else if (val < 100000) { |
| return SkScalarRoundToScalar(a * 10) * (1.0f/10); |
| } else { |
| return SkScalarFloorToScalar(a); |
| } |
| } |
| } // namespace |
| |
| // Since we allow cluster clipping when they don't fit |
| // we have to work with stretches - parts of clusters |
| void TextWrapper::lookAhead(SkScalar maxWidth, Cluster* endOfClusters) { |
| |
| reset(); |
| fEndLine.metrics().clean(); |
| fWords.startFrom(fEndLine.startCluster(), fEndLine.startPos()); |
| fClusters.startFrom(fEndLine.startCluster(), fEndLine.startPos()); |
| fClip.startFrom(fEndLine.startCluster(), fEndLine.startPos()); |
| |
| for (auto cluster = fEndLine.endCluster(); cluster < endOfClusters; ++cluster) { |
| // TODO: Trying to deal with flutter rounding problem. Must be removed... |
| auto width = fWords.width() + fClusters.width() + cluster->width(); |
| auto roundedWidth = littleRound(width); |
| if (cluster->isHardBreak()) { |
| } else if (roundedWidth > maxWidth) { |
| if (cluster->isWhitespaces()) { |
| // It's the end of the word |
| fClusters.extend(cluster); |
| fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, getClustersTrimmedWidth()); |
| fWords.extend(fClusters); |
| break; |
| } else if (cluster->run()->isPlaceholder()) { |
| if (!fClusters.empty()) { |
| // Placeholder ends the previous word |
| fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, getClustersTrimmedWidth()); |
| fWords.extend(fClusters); |
| } |
| |
| if (cluster->width() > maxWidth) { |
| // Placeholder is longer than the line; it does not count in fMinIntrinsicWidth |
| fClusters.extend(cluster); |
| fTooLongCluster = true; |
| fTooLongWord = true; |
| } else { |
| // Placeholder does not fit the line; it will be considered again on the next line |
| } |
| break; |
| } |
| |
| // Walk further to see if there is a too long word, cluster or glyph |
| SkScalar nextWordLength = fClusters.width(); |
| for (auto further = cluster; further != endOfClusters; ++further) { |
| if (further->isSoftBreak() || further->isHardBreak() || further->isWhitespaces()) { |
| break; |
| } |
| if (further->run()->isPlaceholder()) { |
| // Placeholder ends the word |
| break; |
| } |
| if (maxWidth == 0) { |
| // This is a tricky flutter case: layout(width:0) places 1 cluster on each line |
| nextWordLength = std::max(nextWordLength, further->width()); |
| } else { |
| nextWordLength += further->width(); |
| } |
| } |
| if (nextWordLength > maxWidth) { |
| // If the word is too long we can break it right now and hope it's enough |
| fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, nextWordLength); |
| if (fClusters.endPos() - fClusters.startPos() > 1 || |
| fWords.empty()) { |
| fTooLongWord = true; |
| } else { |
| // Even if the word is too long there is a very little space on this line. |
| // let's deal with it on the next line. |
| } |
| } |
| |
| if (cluster->width() > maxWidth) { |
| fClusters.extend(cluster); |
| fTooLongCluster = true; |
| fTooLongWord = true; |
| } |
| break; |
| } |
| |
| if (cluster->run()->isPlaceholder()) { |
| if (!fClusters.empty()) { |
| // Placeholder ends the previous word |
| fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, getClustersTrimmedWidth()); |
| fWords.extend(fClusters); |
| } |
| |
| // It also creates a separate word; it does not count in fMinIntrinsicWidth |
| fWords.extend(cluster); |
| continue; |
| } |
| |
| fClusters.extend(cluster); |
| |
| // Keep adding clusters/words |
| if (fClusters.endOfWord()) { |
| fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, getClustersTrimmedWidth()); |
| fWords.extend(fClusters); |
| } |
| |
| if ((fHardLineBreak = cluster->isHardBreak())) { |
| // Stop at the hard line break |
| break; |
| } |
| } |
| } |
| |
| void TextWrapper::moveForward(bool hasEllipsis) { |
| |
| // We normally break lines by words. |
| // The only way we may go to clusters is if the word is too long or |
| // it's the first word and it has an ellipsis attached to it. |
| // If nothing fits we show the clipping. |
| if (!fWords.empty()) { |
| fEndLine.extend(fWords); |
| if (!fTooLongWord || hasEllipsis) { |
| return; |
| } |
| } |
| if (!fClusters.empty()) { |
| fEndLine.extend(fClusters); |
| if (!fTooLongCluster) { |
| return; |
| } |
| } |
| |
| if (!fClip.empty()) { |
| // Flutter: forget the clipped cluster but keep the metrics |
| fEndLine.metrics().add(fClip.metrics()); |
| } |
| } |
| |
| // Special case for start/end cluster since they can be clipped |
| void TextWrapper::trimEndSpaces(TextAlign align) { |
| // Remember the breaking position |
| fEndLine.saveBreak(); |
| // Skip all space cluster at the end |
| for (auto cluster = fEndLine.endCluster(); |
| cluster >= fEndLine.startCluster() && cluster->isWhitespaces(); |
| --cluster) { |
| fEndLine.trim(cluster); |
| } |
| fEndLine.trim(); |
| } |
| |
| SkScalar TextWrapper::getClustersTrimmedWidth() { |
| // Move the end of the line to the left |
| SkScalar width = 0; |
| bool trailingSpaces = true; |
| for (auto cluster = fClusters.endCluster(); cluster >= fClusters.startCluster(); --cluster) { |
| if (cluster->run()->isPlaceholder()) { |
| continue; |
| } |
| if (trailingSpaces) { |
| if (!cluster->isWhitespaces()) { |
| width += cluster->trimmedWidth(cluster->endPos()); |
| trailingSpaces = false; |
| } |
| continue; |
| } |
| width += cluster->width(); |
| } |
| return width; |
| } |
| |
| // Trim the beginning spaces in case of soft line break |
| std::tuple<Cluster*, size_t, SkScalar> TextWrapper::trimStartSpaces(Cluster* endOfClusters) { |
| |
| if (fHardLineBreak) { |
| // End of line is always end of cluster, but need to skip \n |
| auto width = fEndLine.width(); |
| auto cluster = fEndLine.endCluster() + 1; |
| while (cluster < fEndLine.breakCluster() && cluster->isWhitespaces()) { |
| width += cluster->width(); |
| ++cluster; |
| } |
| return std::make_tuple(fEndLine.breakCluster() + 1, 0, width); |
| } |
| |
| auto width = fEndLine.widthWithGhostSpaces(); |
| auto cluster = fEndLine.breakCluster(); |
| if (fEndLine.endCluster() != fEndLine.startCluster() || |
| fEndLine.endPos() != fEndLine.startPos()) { |
| ++cluster; |
| while (cluster < endOfClusters && cluster->isWhitespaces()) { |
| width += cluster->width(); |
| ++cluster; |
| } |
| } else { |
| // Nothing fits the line - no need to check for spaces |
| } |
| |
| return std::make_tuple(cluster, 0, width); |
| } |
| |
| // TODO: refactor the code for line ending (with/without ellipsis) |
| void TextWrapper::breakTextIntoLines(ParagraphImpl* parent, |
| SkScalar maxWidth, |
| const AddLineToParagraph& addLine) { |
| fHeight = 0; |
| fMinIntrinsicWidth = std::numeric_limits<SkScalar>::min(); |
| fMaxIntrinsicWidth = std::numeric_limits<SkScalar>::min(); |
| |
| auto span = parent->clusters(); |
| if (span.size() == 0) { |
| return; |
| } |
| auto maxLines = parent->paragraphStyle().getMaxLines(); |
| auto align = parent->paragraphStyle().effective_align(); |
| auto unlimitedLines = maxLines == std::numeric_limits<size_t>::max(); |
| auto endlessLine = !SkScalarIsFinite(maxWidth); |
| auto hasEllipsis = parent->paragraphStyle().ellipsized(); |
| |
| SkScalar softLineMaxIntrinsicWidth = 0; |
| fEndLine = TextStretch(span.begin(), span.begin(), parent->strutForceHeight()); |
| auto end = span.end() - 1; |
| auto start = span.begin(); |
| InternalLineMetrics maxRunMetrics; |
| bool needEllipsis = false; |
| while (fEndLine.endCluster() != end) { |
| |
| lookAhead(maxWidth, end); |
| |
| auto lastLine = (hasEllipsis && unlimitedLines) || fLineNumber >= maxLines; |
| needEllipsis = hasEllipsis && !endlessLine && lastLine; |
| |
| moveForward(needEllipsis); |
| needEllipsis &= fEndLine.endCluster() < end - 1; // Only if we have some text to ellipsize |
| |
| // Do not trim end spaces on the naturally last line of the left aligned text |
| trimEndSpaces(align); |
| |
| // For soft line breaks add to the line all the spaces next to it |
| Cluster* startLine; |
| size_t pos; |
| SkScalar widthWithSpaces; |
| std::tie(startLine, pos, widthWithSpaces) = trimStartSpaces(end); |
| |
| if (needEllipsis && !fHardLineBreak) { |
| // This is what we need to do to preserve a space before the ellipsis |
| fEndLine.restoreBreak(); |
| widthWithSpaces = fEndLine.widthWithGhostSpaces(); |
| } |
| |
| // If the line is empty with the hard line break, let's take the paragraph font (flutter???) |
| if (fHardLineBreak && fEndLine.width() == 0) { |
| fEndLine.setMetrics(parent->getEmptyMetrics()); |
| } |
| |
| // Deal with placeholder clusters == runs[@size==1] |
| Run* lastRun = nullptr; |
| for (auto cluster = fEndLine.startCluster(); cluster <= fEndLine.endCluster(); ++cluster) { |
| if (cluster->run() == lastRun) { |
| continue; |
| } |
| lastRun = cluster->run(); |
| if (lastRun->placeholderStyle() != nullptr) { |
| SkASSERT(lastRun->size() == 1); |
| // Update the placeholder metrics so we can get the placeholder positions later |
| // and the line metrics (to make sure the placeholder fits) |
| lastRun->updateMetrics(&fEndLine.metrics()); |
| } |
| } |
| |
| // Before we update the line metrics with struts, |
| // let's save it for GetRectsForRange(RectHeightStyle::kMax) |
| maxRunMetrics = fEndLine.metrics(); |
| maxRunMetrics.fForceStrut = false; |
| |
| if (parent->strutEnabled()) { |
| // Make sure font metrics are not less than the strut |
| parent->strutMetrics().updateLineMetrics(fEndLine.metrics()); |
| } |
| |
| // TODO: keep start/end/break info for text and runs but in a better way that below |
| TextRange text(fEndLine.startCluster()->textRange().start, fEndLine.endCluster()->textRange().end); |
| TextRange textWithSpaces(fEndLine.startCluster()->textRange().start, startLine->textRange().start); |
| if (startLine == end) { |
| textWithSpaces.end = parent->text().size(); |
| } |
| ClusterRange clusters(fEndLine.startCluster() - start, fEndLine.endCluster() - start + 1); |
| ClusterRange clustersWithGhosts(fEndLine.startCluster() - start, startLine - start); |
| addLine(text, textWithSpaces, clusters, clustersWithGhosts, widthWithSpaces, |
| fEndLine.startPos(), |
| fEndLine.endPos(), |
| SkVector::Make(0, fHeight), |
| SkVector::Make(fEndLine.width(), fEndLine.metrics().height()), |
| fEndLine.metrics(), |
| needEllipsis && !fHardLineBreak); |
| |
| softLineMaxIntrinsicWidth += widthWithSpaces; |
| |
| fMaxIntrinsicWidth = std::max(fMaxIntrinsicWidth, softLineMaxIntrinsicWidth); |
| if (fHardLineBreak) { |
| softLineMaxIntrinsicWidth = 0; |
| } |
| // Start a new line |
| fHeight += fEndLine.metrics().height(); |
| if (!fHardLineBreak || startLine != end) { |
| fEndLine.clean(); |
| } |
| fEndLine.startFrom(startLine, pos); |
| parent->fMaxWidthWithTrailingSpaces = std::max(parent->fMaxWidthWithTrailingSpaces, widthWithSpaces); |
| |
| if (hasEllipsis && unlimitedLines) { |
| // There is one case when we need an ellipsis on a separate line |
| // after a line break when width is infinite |
| if (!fHardLineBreak) { |
| break; |
| } |
| } else if (lastLine) { |
| // There is nothing more to draw |
| fHardLineBreak = false; |
| break; |
| } |
| |
| ++fLineNumber; |
| } |
| |
| // We finished formatting the text but we need to scan the rest for some numbers |
| // TODO: make it a case of a normal flow |
| if (fEndLine.endCluster() != nullptr) { |
| auto lastWordLength = 0.0f; |
| auto cluster = fEndLine.endCluster(); |
| while (cluster != end || cluster->endPos() < end->endPos()) { |
| fExceededMaxLines = true; |
| if (cluster->isHardBreak()) { |
| // Hard line break ends the word and the line |
| fMaxIntrinsicWidth = std::max(fMaxIntrinsicWidth, softLineMaxIntrinsicWidth); |
| softLineMaxIntrinsicWidth = 0; |
| fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength); |
| lastWordLength = 0; |
| } else if (cluster->isWhitespaces()) { |
| // Whitespaces end the word |
| softLineMaxIntrinsicWidth += cluster->width(); |
| fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength); |
| lastWordLength = 0; |
| } else if (cluster->run()->isPlaceholder()) { |
| // Placeholder ends the previous word and creates a separate one |
| // it does not count in fMinIntrinsicWidth |
| softLineMaxIntrinsicWidth += cluster->width(); |
| fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength); |
| lastWordLength = 0; |
| } else { |
| // Nothing out of ordinary - just add this cluster to the word and to the line |
| softLineMaxIntrinsicWidth += cluster->width(); |
| lastWordLength += cluster->width(); |
| } |
| ++cluster; |
| } |
| fMinIntrinsicWidth = std::max(fMinIntrinsicWidth, lastWordLength); |
| fMaxIntrinsicWidth = std::max(fMaxIntrinsicWidth, softLineMaxIntrinsicWidth); |
| // In case we could not place a single cluster on the line |
| fHeight = std::max(fHeight, fEndLine.metrics().height()); |
| } |
| |
| if (fHardLineBreak) { |
| // Last character is a line break |
| if (parent->strutEnabled()) { |
| // Make sure font metrics are not less than the strut |
| parent->strutMetrics().updateLineMetrics(fEndLine.metrics()); |
| } |
| TextRange empty(fEndLine.breakCluster()->textRange().start, fEndLine.breakCluster()->textRange().start); |
| TextRange hardBreak(fEndLine.breakCluster()->textRange().end, fEndLine.breakCluster()->textRange().end); |
| ClusterRange clusters(fEndLine.breakCluster() - start, fEndLine.endCluster() - start); |
| addLine(empty, hardBreak, clusters, clusters, |
| 0, |
| 0, |
| 0, |
| SkVector::Make(0, fHeight), |
| SkVector::Make(0, fEndLine.metrics().height()), |
| fEndLine.metrics(), |
| needEllipsis); |
| fHeight += fEndLine.metrics().height(); |
| parent->lines().back().setMaxRunMetrics(maxRunMetrics); |
| } |
| } |
| |
| } // namespace textlayout |
| } // namespace skia |