Hide more of leftToRight checks

Moving some code down from paragraph to line.

Change-Id: I9408951fe8d05a5956e4bbe4b50c9ef3f3dc1f9c
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/285838
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Julia Lavrova <jlavrova@google.com>
This commit is contained in:
Julia Lavrova 2020-04-27 15:49:53 -04:00 committed by Skia Commit-Bot
parent b32d66b296
commit 8335ab694d
6 changed files with 427 additions and 370 deletions

View File

@ -183,6 +183,7 @@ void ParagraphImpl::layout(SkScalar rawWidth) {
fState = kClusterized;
this->markLineBreaks();
this->spaceGlyphs();
fState = kMarked;
}
@ -253,8 +254,8 @@ void ParagraphImpl::resetContext() {
void ParagraphImpl::buildClusterTable() {
// Walk through all the run in the direction of input text
for (RunIndex runIndex = 0; runIndex < fRuns.size(); ++runIndex) {
auto& run = fRuns[runIndex];
for (auto& run : fRuns) {
auto runIndex = run.index();
auto runStart = fClusters.size();
if (run.isPlaceholder()) {
// There are no glyphs but we want to have one cluster
@ -288,6 +289,53 @@ void ParagraphImpl::buildClusterTable() {
run.setClusterRange(runStart, fClusters.size());
fMaxIntrinsicWidth += run.advance().fX;
}
fClusters.emplace_back(this, EMPTY_RUN, 0, 0, SkSpan<const char>(), 0, 0);
}
void ParagraphImpl::spaceGlyphs() {
// Walk through all the clusters in the direction of shaped text
// (we have to walk through the styles in the same order, too)
SkScalar shift = 0;
for (auto& run : fRuns) {
// Skip placeholder runs
if (run.isPlaceholder()) {
continue;
}
bool soFarWhitespacesOnly = true;
run.iterateThroughClusters([this, &run, &shift, &soFarWhitespacesOnly](Cluster* cluster) {
// Shift the cluster (shift collected from the previous clusters)
run.shift(cluster, shift);
// Synchronize styles (one cluster can be covered by few styles)
Block* currentStyle = this->fTextStyles.begin();
while (!cluster->startsIn(currentStyle->fRange)) {
currentStyle++;
SkASSERT(currentStyle != this->fTextStyles.end());
}
SkASSERT(!currentStyle->fStyle.isPlaceholder());
// Process word spacing
if (currentStyle->fStyle.getWordSpacing() != 0) {
if (cluster->isWhitespaces() && cluster->isSoftBreak()) {
if (!soFarWhitespacesOnly) {
shift += run.addSpacesAtTheEnd(currentStyle->fStyle.getWordSpacing(), cluster);
}
}
}
// Process letter spacing
if (currentStyle->fStyle.getLetterSpacing() != 0) {
shift += run.addSpacesEvenly(currentStyle->fStyle.getLetterSpacing(), cluster);
}
if (soFarWhitespacesOnly && !cluster->isWhitespaces()) {
soFarWhitespacesOnly = false;
}
});
}
}
void ParagraphImpl::markLineBreaks() {
@ -323,56 +371,6 @@ void ParagraphImpl::markLineBreaks() {
++current;
}
}
// Walk through all the clusters in the direction of shaped text
// (we have to walk through the styles in the same order, too)
SkScalar shift = 0;
for (auto& run : fRuns) {
// Skip placeholder runs
if (run.isPlaceholder()) {
continue;
}
bool soFarWhitespacesOnly = true;
for (size_t index = 0; index != run.clusterRange().width(); ++index) {
auto correctIndex = run.leftToRight()
? index + run.clusterRange().start
: run.clusterRange().end - index - 1;
const auto cluster = &this->cluster(correctIndex);
// Shift the cluster (shift collected from the previous clusters)
run.shift(cluster, shift);
// Synchronize styles (one cluster can be covered by few styles)
Block* currentStyle = this->fTextStyles.begin();
while (!cluster->startsIn(currentStyle->fRange)) {
currentStyle++;
SkASSERT(currentStyle != this->fTextStyles.end());
}
SkASSERT(!currentStyle->fStyle.isPlaceholder());
// Process word spacing
if (currentStyle->fStyle.getWordSpacing() != 0) {
if (cluster->isWhitespaces() && cluster->isSoftBreak()) {
if (!soFarWhitespacesOnly) {
shift += run.addSpacesAtTheEnd(currentStyle->fStyle.getWordSpacing(), cluster);
}
}
}
// Process letter spacing
if (currentStyle->fStyle.getLetterSpacing() != 0) {
shift += run.addSpacesEvenly(currentStyle->fStyle.getLetterSpacing(), cluster);
}
if (soFarWhitespacesOnly && !cluster->isWhitespaces()) {
soFarWhitespacesOnly = false;
}
}
}
fClusters.emplace_back(this, EMPTY_RUN, 0, 0, SkSpan<const char>(), 0, 0);
}
bool ParagraphImpl::shapeTextIntoEndlessLine() {
@ -656,192 +654,14 @@ std::vector<TextBox> ParagraphImpl::getRectsForRange(unsigned start,
text.end = grapheme.fTextRange.start;
}
auto firstBoxOnTheLine = results.size();
const Run* lastRun = nullptr;
auto paragraphTextDirection = paragraphStyle().getTextDirection();
for (auto& line : fLines) {
auto lineText = line.textWithSpaces();
auto intersect = lineText * text;
auto lastLine = (&line == &fLines.back());
if (intersect.empty() && lineText.start != text.start) {
continue;
}
line.iterateThroughVisualRuns(true,
[&]
(const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
*runWidthInLine = line.iterateThroughSingleRunByStyles(
run, runOffsetInLine, textRange, StyleType::kNone,
[&]
(TextRange textRange, const TextStyle& style, const TextLine::ClipContext& context0) {
auto intersect = textRange * text;
if (intersect.empty()) {
return true;
}
// Found a run that intersects with the text
auto context = line.measureTextInsideOneRun(intersect, run, runOffsetInLine, 0, true, true);
SkRect clip = context.clip;
clip.offset(context0.fTextShift - context.fTextShift, 0);
switch (rectHeightStyle) {
case RectHeightStyle::kMax:
// TODO: Change it once flutter rolls into google3
// (probably will break things if changed before)
clip.fBottom = line.height();
clip.fTop = line.sizes().delta();
break;
case RectHeightStyle::kIncludeLineSpacingTop:
if (&line != &fLines.front()) {
clip.fTop -= line.sizes().runTop(context.run);
}
clip.fBottom -= line.sizes().runTop(context.run);
break;
case RectHeightStyle::kIncludeLineSpacingMiddle:
if (&line != &fLines.front()) {
clip.fTop -= line.sizes().runTop(context.run) / 2;
}
if (&line == &fLines.back()) {
clip.fBottom -= line.sizes().runTop(context.run);
} else {
clip.fBottom -= line.sizes().runTop(context.run) / 2;
}
break;
case RectHeightStyle::kIncludeLineSpacingBottom:
if (&line == &fLines.back()) {
clip.fBottom -= line.sizes().runTop(context.run);
}
break;
case RectHeightStyle::kStrut: {
auto strutStyle = this->paragraphStyle().getStrutStyle();
if (strutStyle.getStrutEnabled()
&& strutStyle.getFontSize() > 0) {
auto top = line.baseline();
clip.fTop = top + fStrutMetrics.ascent();
clip.fBottom = top + fStrutMetrics.descent();
}
}
break;
case RectHeightStyle::kTight:
if (run->fHeightMultiplier > 0) {
// This is a special case when we do not need to take in account this height multiplier
clip.fBottom = clip.fTop + clip.height() / run->fHeightMultiplier;
}
break;
default:
SkASSERT(false);
break;
}
// Separate trailing spaces and move them in the default order of the paragraph
// in case the run order and the paragraph order don't match
SkRect trailingSpaces = SkRect::MakeEmpty();
if (line.trimmedText().end < line.textWithSpaces().end && // Line has trailing spaces
line.textWithSpaces().end == intersect.end && // Range is at the end of the line
line.trimmedText().end > intersect.start) // Range has more than just spaces
{
auto delta = line.spacesWidth();
trailingSpaces = SkRect::MakeXYWH(0, 0, 0, 0);
// There are trailing spaces in this run
if (this->paragraphStyle().getTextAlign() == TextAlign::kJustify &&
&line != &fLines.back())
{
// TODO: this is just a patch. Make it right later (when it's clear what and how)
trailingSpaces = clip;
if(run->leftToRight()) {
trailingSpaces.fLeft = line.width();
clip.fRight = line.width();
} else {
trailingSpaces.fRight = 0;
clip.fLeft = 0;
}
} else if (this->fParagraphStyle.getTextDirection() == TextDirection::kRtl &&
!run->leftToRight())
{
// Split
trailingSpaces = clip;
trailingSpaces.fLeft = - delta;
trailingSpaces.fRight = 0;
clip.fLeft += delta;
} else if (this->fParagraphStyle.getTextDirection() == TextDirection::kLtr &&
run->leftToRight())
{
// Split
trailingSpaces = clip;
trailingSpaces.fLeft = line.width();
trailingSpaces.fRight = trailingSpaces.fLeft + delta;
clip.fRight -= delta;
}
}
clip.offset(line.offset());
if (trailingSpaces.width() > 0) {
trailingSpaces.offset(line.offset());
}
// Check if we can merge two boxes instead of adding a new one
auto merge = [&lastRun, &context, &results](SkRect clip) {
bool mergedBoxes = false;
if (!results.empty() &&
lastRun != nullptr &&
lastRun->placeholderStyle() == nullptr &&
context.run->placeholderStyle() == nullptr &&
nearlyEqual(lastRun->lineHeight(), context.run->lineHeight()) &&
lastRun->font() == context.run->font())
{
auto& lastBox = results.back();
if (nearlyEqual(lastBox.rect.fTop, clip.fTop) &&
nearlyEqual(lastBox.rect.fBottom, clip.fBottom) &&
(nearlyEqual(lastBox.rect.fLeft, clip.fRight) ||
nearlyEqual(lastBox.rect.fRight, clip.fLeft)))
{
lastBox.rect.fLeft = std::min(lastBox.rect.fLeft, clip.fLeft);
lastBox.rect.fRight = std::max(lastBox.rect.fRight, clip.fRight);
mergedBoxes = true;
}
}
lastRun = context.run;
return mergedBoxes;
};
if (!merge(clip)) {
results.emplace_back(
clip, context.run->leftToRight() ? TextDirection::kLtr : TextDirection::kRtl);
}
if (!nearlyZero(trailingSpaces.width()) && !merge(trailingSpaces)) {
results.emplace_back(trailingSpaces, paragraphTextDirection);
}
return true;
});
return true;
});
if (rectWidthStyle == RectWidthStyle::kMax && !lastLine) {
// Align the very left/right box horizontally
auto lineStart = line.offset().fX;
auto lineEnd = line.offset().fX + line.width();
auto left = results.front();
auto right = results.back();
if (left.rect.fLeft > lineStart && left.direction == TextDirection::kRtl) {
left.rect.fRight = left.rect.fLeft;
left.rect.fLeft = 0;
results.insert(results.begin() + firstBoxOnTheLine + 1, left);
}
if (right.direction == TextDirection::kLtr &&
right.rect.fRight >= lineEnd && right.rect.fRight < this->fMaxWidthWithTrailingSpaces) {
right.rect.fLeft = right.rect.fRight;
right.rect.fRight = this->fMaxWidthWithTrailingSpaces;
results.emplace_back(right);
}
}
for (auto& r : results) {
r.rect.fLeft = littleRound(r.rect.fLeft);
r.rect.fRight = littleRound(r.rect.fRight);
r.rect.fTop = littleRound(r.rect.fTop);
r.rect.fBottom = littleRound(r.rect.fBottom);
}
line.getRectsForRange(intersect, rectHeightStyle, rectWidthStyle, results);
}
/*
SkDebugf("getRectsForRange(%d, %d)\n", start, end);
@ -866,31 +686,7 @@ std::vector<TextBox> ParagraphImpl::getRectsForPlaceholders() {
return boxes;
}
for (auto& line : fLines) {
line.iterateThroughVisualRuns(
true,
[&boxes, &line](const Run* run, SkScalar runOffset, TextRange textRange,
SkScalar* width) {
auto context = line.measureTextInsideOneRun(textRange, run, runOffset, 0, true, false);
*width = context.clip.width();
if (textRange.width() == 0) {
return true;
}
if (!run->isPlaceholder()) {
return true;
}
SkRect clip = context.clip;
clip.offset(line.offset());
clip.fLeft = littleRound(clip.fLeft);
clip.fRight = littleRound(clip.fRight);
clip.fTop = littleRound(clip.fTop);
clip.fBottom = littleRound(clip.fBottom);
boxes.emplace_back(
clip, run->leftToRight() ? TextDirection::kLtr : TextDirection::kRtl);
return true;
});
line.getRectsForPlaceholders(boxes);
}
/*
SkDebugf("getRectsForPlaceholders('%s'): %d\n", fText.c_str(), boxes.size());
@ -909,9 +705,8 @@ std::vector<TextBox> ParagraphImpl::getRectsForPlaceholders() {
// TODO: Optimize (save cluster <-> codepoint connection)
PositionWithAffinity ParagraphImpl::getGlyphPositionAtCoordinate(SkScalar dx, SkScalar dy) {
PositionWithAffinity result(0, Affinity::kDownstream);
if (fText.isEmpty()) {
return result;
return {0, Affinity::kDownstream};
}
markGraphemes16();
@ -925,112 +720,14 @@ PositionWithAffinity ParagraphImpl::getGlyphPositionAtCoordinate(SkScalar dx, Sk
// This is so far the the line vertically closest to our coordinates
// (or the first one, or the only one - all the same)
line.iterateThroughVisualRuns(true,
[this, &line, dx, &result]
(const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
bool lookingForHit = true;
*runWidthInLine = line.iterateThroughSingleRunByStyles(
run, runOffsetInLine, textRange, StyleType::kNone,
[this, line, dx, &result, &lookingForHit]
(TextRange textRange, const TextStyle& style, const TextLine::ClipContext& context) {
auto findCodepointByTextIndex = [this](ClusterIndex clusterIndex8) {
auto codepoint = std::lower_bound(
fCodePoints.begin(), fCodePoints.end(),
clusterIndex8,
[](const Codepoint& lhs,size_t rhs) -> bool { return lhs.fTextIndex < rhs; });
return codepoint - fCodePoints.begin();
};
auto offsetX = line.offset().fX;
if (dx < context.clip.fLeft + offsetX) {
// All the other runs are placed right of this one
auto codepointIndex = findCodepointByTextIndex(context.run->globalClusterIndex(context.pos));
result = { SkToS32(codepointIndex), kDownstream };
lookingForHit = false;
return false;
}
if (dx >= context.clip.fRight + offsetX) {
// We have to keep looking ; just in case keep the last one as the closest
auto codepointIndex = findCodepointByTextIndex(context.run->globalClusterIndex(context.pos + context.size));
result = { SkToS32(codepointIndex), kUpstream };
return true;
}
// So we found the run that contains our coordinates
// Find the glyph position in the run that is the closest left of our point
// TODO: binary search
size_t found = context.pos;
for (size_t index = context.pos; index < context.pos + context.size; ++index) {
// TODO: this rounding is done to match Flutter tests. Must be removed..
auto end = littleRound(context.run->positionX(index) + context.fTextShift + offsetX);
if (end > dx) {
break;
}
found = index;
}
auto glyphStart = context.run->positionX(found) + context.fTextShift + offsetX;
auto glyphWidth = context.run->positionX(found + 1) - context.run->positionX(found);
auto clusterIndex8 = context.run->globalClusterIndex(found);
auto clusterEnd8 = context.run->globalClusterIndex(found + 1);
// Find the grapheme positions in codepoints that contains the point
auto codepointIndex = findCodepointByTextIndex(clusterIndex8);
CodepointRange codepoints(codepointIndex, codepointIndex);
if (context.run->leftToRight()) {
for (codepoints.end = codepointIndex;
codepoints.end < fCodePoints.size(); ++codepoints.end) {
auto& cp = fCodePoints[codepoints.end];
if (cp.fTextIndex >= clusterEnd8) {
break;
}
}
} else {
for (codepoints.end = codepointIndex;
codepoints.end > 0; --codepoints.end) {
auto& cp = fCodePoints[codepoints.end];
if (cp.fTextIndex <= clusterEnd8) {
break;
}
}
std::swap(codepoints.start, codepoints.end);
}
auto graphemeSize = codepoints.width();
// We only need to inspect one glyph (maybe not even the entire glyph)
SkScalar center;
bool insideGlyph = false;
if (graphemeSize > 1) {
auto averageCodepointWidth = glyphWidth / graphemeSize;
auto delta = dx - glyphStart;
auto insideIndex = SkScalarFloorToInt(delta / averageCodepointWidth);
insideGlyph = delta > averageCodepointWidth;
center = glyphStart + averageCodepointWidth * insideIndex + averageCodepointWidth / 2;
codepointIndex += insideIndex;
} else {
center = glyphStart + glyphWidth / 2;
}
if ((dx < center) == context.run->leftToRight() || insideGlyph) {
result = { SkToS32(codepointIndex), kDownstream };
} else {
result = { SkToS32(codepointIndex + 1), kUpstream };
}
// No need to continue
lookingForHit = false;
return false;
});
return lookingForHit;
});
break;
auto result = line.getGlyphPositionAtCoordinate(dx);
//SkDebugf("getGlyphPositionAtCoordinate(%f, %f): %d %s\n", dx, dy, result.position,
// result.affinity == Affinity::kUpstream ? "up" : "down");
return result;
}
//SkDebugf("getGlyphPositionAtCoordinate(%f, %f): %d %s\n", dx, dy, result.position,
// result.affinity == Affinity::kUpstream ? "up" : "down");
return result;
return {0, Affinity::kDownstream};
}
// Finds the first and last glyphs that define a word containing

View File

@ -143,6 +143,7 @@ public:
SkSpan<Cluster> clusters() { return SkSpan<Cluster>(fClusters.begin(), fClusters.size()); }
sk_sp<FontCollection> fontCollection() const { return fFontCollection; }
const SkTHashSet<size_t>& graphemes() const { return fGraphemes; }
SkSpan<Codepoint> codepoints(){ return SkSpan<Codepoint>(fCodePoints.begin(), fCodePoints.size()); }
void formatLines(SkScalar maxWidth);
bool strutEnabled() const { return paragraphStyle().getStrutStyle().getStrutEnabled(); }
@ -171,10 +172,13 @@ public:
sk_sp<SkPicture> getPicture() { return fPicture; }
SkRect getBoundaries() const { return fOrigin; }
SkScalar widthWithTrailingSpaces() { return fMaxWidthWithTrailingSpaces; }
void resetContext();
void resolveStrut();
void buildClusterTable();
void markLineBreaks();
void spaceGlyphs();
bool shapeTextIntoEndlessLine();
void breakShapedTextIntoLines(SkScalar maxWidth);
void paintLinesIntoPicture();

View File

@ -151,7 +151,7 @@ std::tuple<bool, ClusterIndex, ClusterIndex> Run::findLimitingClusters(TextRange
return std::make_tuple(startIndex != fClusterRange.end && endIndex != fClusterRange.end, startIndex, endIndex);
}
void Run::iterateThroughClustersInTextOrder(const ClusterVisitor& visitor) {
void Run::iterateThroughClustersInTextOrder(const ClusterTextVisitor& visitor) {
// Can't figure out how to do it with one code for both cases without 100 ifs
// Can't go through clusters because there are no cluster table yet
if (leftToRight()) {
@ -196,6 +196,15 @@ void Run::iterateThroughClustersInTextOrder(const ClusterVisitor& visitor) {
}
}
void Run::iterateThroughClusters(const ClusterVisitor& visitor) {
for (size_t index = 0; index < fClusterRange.width(); ++index) {
auto correctIndex = leftToRight() ? fClusterRange.start + index : fClusterRange.end - index - 1;
auto cluster = &fMaster->cluster(correctIndex);
visitor(cluster);
}
}
SkScalar Run::addSpacesAtTheEnd(SkScalar space, Cluster* cluster) {
if (cluster->endPos() == cluster->startPos()) {
return 0;

View File

@ -107,6 +107,7 @@ public:
}
const SkFont& font() const { return fFont; }
bool leftToRight() const { return fBidiLevel % 2 == 0; }
TextDirection getTextDirection() const { return leftToRight() ? TextDirection::kLtr : TextDirection::kRtl; }
size_t index() const { return fIndex; }
SkScalar lineHeight() const { return fHeightMultiplier; }
PlaceholderStyle* placeholderStyle() const;
@ -143,13 +144,16 @@ public:
void copyTo(SkTextBlobBuilder& builder, size_t pos, size_t size, SkVector offset) const;
using ClusterVisitor = std::function<void(size_t glyphStart,
size_t glyphEnd,
size_t charStart,
size_t charEnd,
SkScalar width,
SkScalar height)>;
void iterateThroughClustersInTextOrder(const ClusterVisitor& visitor);
using ClusterTextVisitor = std::function<void(size_t glyphStart,
size_t glyphEnd,
size_t charStart,
size_t charEnd,
SkScalar width,
SkScalar height)>;
void iterateThroughClustersInTextOrder(const ClusterTextVisitor& visitor);
using ClusterVisitor = std::function<void(Cluster* cluster)>;
void iterateThroughClusters(const ClusterVisitor& visitor);
std::tuple<bool, ClusterIndex, ClusterIndex> findLimitingClusters(TextRange text, bool onlyInnerClusters) const;
SkSpan<const SkGlyphID> glyphs() const {

View File

@ -26,6 +26,13 @@ SkScalar littleRound(SkScalar a) {
return SkScalarRoundToScalar(a * 100.0)/100.0;
}
TextRange operator*(const TextRange& a, const TextRange& b) {
if (a.start == b.start && a.end == b.end) return a;
auto begin = std::max(a.start, b.start);
auto end = std::min(a.end, b.end);
return end > begin ? TextRange(begin, end) : EMPTY_TEXT;
}
int compareRound(SkScalar a, SkScalar b) {
// There is a rounding error that gets bigger when maxWidth gets bigger
// VERY long zalgo text (> 100000) on a VERY long line (> 10000)
@ -992,5 +999,335 @@ LineMetrics TextLine::getMetrics() const {
return result;
}
bool TextLine::isFirstLine() {
return this == &fMaster->lines().front();
}
bool TextLine::isLastLine() {
return this == &fMaster->lines().back();
}
void TextLine::getRectsForRange(TextRange textRange0,
RectHeightStyle rectHeightStyle,
RectWidthStyle rectWidthStyle,
std::vector<TextBox>& boxes)
{
const Run* lastRun = nullptr;
auto startBox = boxes.size();
this->iterateThroughVisualRuns(true,
[textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this]
(const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
*runWidthInLine = this->iterateThroughSingleRunByStyles(
run, runOffsetInLine, textRange, StyleType::kNone,
[run, runOffsetInLine, textRange0, rectHeightStyle, rectWidthStyle, &boxes, &lastRun, startBox, this]
(TextRange textRange, const TextStyle& style, const TextLine::ClipContext& lineContext) {
auto intersect = textRange * textRange0;
if (intersect.empty()) {
return true;
}
auto paragraphStyle = fMaster->paragraphStyle();
// Found a run that intersects with the text
auto context = this->measureTextInsideOneRun(intersect, run, runOffsetInLine, 0, true, true);
SkRect clip = context.clip;
clip.offset(lineContext.fTextShift - context.fTextShift, 0);
switch (rectHeightStyle) {
case RectHeightStyle::kMax:
// TODO: Change it once flutter rolls into google3
// (probably will break things if changed before)
clip.fBottom = this->height();
clip.fTop = this->sizes().delta();
break;
case RectHeightStyle::kIncludeLineSpacingTop:
if (!isFirstLine()) {
clip.fTop -= this->sizes().runTop(context.run);
}
clip.fBottom -= this->sizes().runTop(context.run);
break;
case RectHeightStyle::kIncludeLineSpacingMiddle:
if (!isFirstLine()) {
clip.fTop -= this->sizes().runTop(context.run) / 2;
}
if (isLastLine()) {
clip.fBottom -= this->sizes().runTop(context.run);
} else {
clip.fBottom -= this->sizes().runTop(context.run) / 2;
}
break;
case RectHeightStyle::kIncludeLineSpacingBottom:
if (isLastLine()) {
clip.fBottom -= this->sizes().runTop(context.run);
}
break;
case RectHeightStyle::kStrut: {
auto strutStyle = paragraphStyle.getStrutStyle();
if (strutStyle.getStrutEnabled()
&& strutStyle.getFontSize() > 0) {
auto strutMetrics = fMaster->strutMetrics();
auto top = this->baseline();
clip.fTop = top + strutMetrics.ascent();
clip.fBottom = top + strutMetrics.descent();
}
}
break;
case RectHeightStyle::kTight:
if (run->fHeightMultiplier > 0) {
// This is a special case when we do not need to take in account this height multiplier
clip.fBottom = clip.fTop + clip.height() / run->fHeightMultiplier;
}
break;
default:
SkASSERT(false);
break;
}
// Separate trailing spaces and move them in the default order of the paragraph
// in case the run order and the paragraph order don't match
SkRect trailingSpaces = SkRect::MakeEmpty();
if (this->trimmedText().end < this->textWithSpaces().end && // Line has trailing spaces
this->textWithSpaces().end == intersect.end && // Range is at the end of the line
this->trimmedText().end > intersect.start) // Range has more than just spaces
{
auto delta = this->spacesWidth();
trailingSpaces = SkRect::MakeXYWH(0, 0, 0, 0);
// There are trailing spaces in this run
if (paragraphStyle.getTextAlign() == TextAlign::kJustify && isLastLine())
{
// TODO: this is just a patch. Make it right later (when it's clear what and how)
trailingSpaces = clip;
if(run->leftToRight()) {
trailingSpaces.fLeft = this->width();
clip.fRight = this->width();
} else {
trailingSpaces.fRight = 0;
clip.fLeft = 0;
}
} else if (paragraphStyle.getTextDirection() == TextDirection::kRtl &&
!run->leftToRight())
{
// Split
trailingSpaces = clip;
trailingSpaces.fLeft = - delta;
trailingSpaces.fRight = 0;
clip.fLeft += delta;
} else if (paragraphStyle.getTextDirection() == TextDirection::kLtr &&
run->leftToRight())
{
// Split
trailingSpaces = clip;
trailingSpaces.fLeft = this->width();
trailingSpaces.fRight = trailingSpaces.fLeft + delta;
clip.fRight -= delta;
}
}
clip.offset(this->offset());
if (trailingSpaces.width() > 0) {
trailingSpaces.offset(this->offset());
}
// Check if we can merge two boxes instead of adding a new one
auto merge = [&lastRun, &context, &boxes](SkRect clip) {
bool mergedBoxes = false;
if (!boxes.empty() &&
lastRun != nullptr &&
lastRun->placeholderStyle() == nullptr &&
context.run->placeholderStyle() == nullptr &&
nearlyEqual(lastRun->lineHeight(), context.run->lineHeight()) &&
lastRun->font() == context.run->font())
{
auto& lastBox = boxes.back();
if (nearlyEqual(lastBox.rect.fTop, clip.fTop) &&
nearlyEqual(lastBox.rect.fBottom, clip.fBottom) &&
(nearlyEqual(lastBox.rect.fLeft, clip.fRight) ||
nearlyEqual(lastBox.rect.fRight, clip.fLeft)))
{
lastBox.rect.fLeft = std::min(lastBox.rect.fLeft, clip.fLeft);
lastBox.rect.fRight = std::max(lastBox.rect.fRight, clip.fRight);
mergedBoxes = true;
}
}
lastRun = context.run;
return mergedBoxes;
};
if (!merge(clip)) {
boxes.emplace_back(clip, context.run->getTextDirection());
}
if (!nearlyZero(trailingSpaces.width()) && !merge(trailingSpaces)) {
boxes.emplace_back(trailingSpaces, paragraphStyle.getTextDirection());
}
if (rectWidthStyle == RectWidthStyle::kMax && !isLastLine()) {
// Align the very left/right box horizontally
auto lineStart = this->offset().fX;
auto lineEnd = this->offset().fX + this->width();
auto left = boxes[startBox];
auto right = boxes.back();
if (left.rect.fLeft > lineStart && left.direction == TextDirection::kRtl) {
left.rect.fRight = left.rect.fLeft;
left.rect.fLeft = 0;
boxes.insert(boxes.begin() + startBox + 1, left);
}
if (right.direction == TextDirection::kLtr &&
right.rect.fRight >= lineEnd && right.rect.fRight < fMaster->widthWithTrailingSpaces()) {
right.rect.fLeft = right.rect.fRight;
right.rect.fRight = fMaster->widthWithTrailingSpaces();
boxes.emplace_back(right);
}
}
return true;
});
return true;
});
for (auto& r : boxes) {
r.rect.fLeft = littleRound(r.rect.fLeft);
r.rect.fRight = littleRound(r.rect.fRight);
r.rect.fTop = littleRound(r.rect.fTop);
r.rect.fBottom = littleRound(r.rect.fBottom);
}
}
PositionWithAffinity TextLine::getGlyphPositionAtCoordinate(SkScalar dx) {
PositionWithAffinity result(0, Affinity::kDownstream);
this->iterateThroughVisualRuns(true,
[this, dx, &result]
(const Run* run, SkScalar runOffsetInLine, TextRange textRange, SkScalar* runWidthInLine) {
bool lookingForHit = true;
*runWidthInLine = this->iterateThroughSingleRunByStyles(
run, runOffsetInLine, textRange, StyleType::kNone,
[this, dx, &result, &lookingForHit]
(TextRange textRange, const TextStyle& style, const TextLine::ClipContext& context) {
auto findCodepointByTextIndex = [this](ClusterIndex clusterIndex8) {
auto codepoints = fMaster->codepoints();
auto codepoint = std::lower_bound(
codepoints.begin(), codepoints.end(),
clusterIndex8,
[](const Codepoint& lhs,size_t rhs) -> bool { return lhs.fTextIndex < rhs; });
return codepoint - codepoints.begin();
};
auto offsetX = this->offset().fX;
if (dx < context.clip.fLeft + offsetX) {
// All the other runs are placed right of this one
auto codepointIndex = findCodepointByTextIndex(context.run->globalClusterIndex(context.pos));
result = { SkToS32(codepointIndex), kDownstream };
lookingForHit = false;
return false;
}
if (dx >= context.clip.fRight + offsetX) {
// We have to keep looking ; just in case keep the last one as the closest
auto codepointIndex = findCodepointByTextIndex(context.run->globalClusterIndex(context.pos + context.size));
result = { SkToS32(codepointIndex), kUpstream };
return true;
}
// So we found the run that contains our coordinates
// Find the glyph position in the run that is the closest left of our point
// TODO: binary search
size_t found = context.pos;
for (size_t index = context.pos; index < context.pos + context.size; ++index) {
// TODO: this rounding is done to match Flutter tests. Must be removed..
auto end = littleRound(context.run->positionX(index) + context.fTextShift + offsetX);
if (end > dx) {
break;
}
found = index;
}
auto glyphStart = context.run->positionX(found) + context.fTextShift + offsetX;
auto glyphWidth = context.run->positionX(found + 1) - context.run->positionX(found);
auto clusterIndex8 = context.run->globalClusterIndex(found);
auto clusterEnd8 = context.run->globalClusterIndex(found + 1);
// Find the grapheme positions in codepoints that contains the point
auto codepointIndex = findCodepointByTextIndex(clusterIndex8);
CodepointRange codepoints(codepointIndex, codepointIndex);
auto masterCodepoints = fMaster->codepoints();
if (context.run->leftToRight()) {
for (codepoints.end = codepointIndex;
codepoints.end < masterCodepoints.size(); ++codepoints.end) {
auto& cp = masterCodepoints[codepoints.end];
if (cp.fTextIndex >= clusterEnd8) {
break;
}
}
} else {
for (codepoints.end = codepointIndex;
codepoints.end > 0; --codepoints.end) {
auto& cp = masterCodepoints[codepoints.end];
if (cp.fTextIndex <= clusterEnd8) {
break;
}
}
std::swap(codepoints.start, codepoints.end);
}
auto graphemeSize = codepoints.width();
// We only need to inspect one glyph (maybe not even the entire glyph)
SkScalar center;
bool insideGlyph = false;
if (graphemeSize > 1) {
auto averageCodepointWidth = glyphWidth / graphemeSize;
auto delta = dx - glyphStart;
auto insideIndex = SkScalarFloorToInt(delta / averageCodepointWidth);
insideGlyph = delta > averageCodepointWidth;
center = glyphStart + averageCodepointWidth * insideIndex + averageCodepointWidth / 2;
codepointIndex += insideIndex;
} else {
center = glyphStart + glyphWidth / 2;
}
if ((dx < center) == context.run->leftToRight() || insideGlyph) {
result = { SkToS32(codepointIndex), kDownstream };
} else {
result = { SkToS32(codepointIndex + 1), kUpstream };
}
// No need to continue
lookingForHit = false;
return false;
});
return lookingForHit;
}
);
return result;
}
void TextLine::getRectsForPlaceholders(std::vector<TextBox>& boxes) {
this->iterateThroughVisualRuns(
true,
[&boxes, this](const Run* run, SkScalar runOffset, TextRange textRange,
SkScalar* width) {
auto context = this->measureTextInsideOneRun(textRange, run, runOffset, 0, true, false);
*width = context.clip.width();
if (textRange.width() == 0) {
return true;
}
if (!run->isPlaceholder()) {
return true;
}
SkRect clip = context.clip;
clip.offset(this->offset());
clip.fLeft = littleRound(clip.fLeft);
clip.fRight = littleRound(clip.fRight);
clip.fTop = littleRound(clip.fTop);
clip.fBottom = littleRound(clip.fBottom);
boxes.emplace_back(clip, run->getTextDirection());
return true;
});
}
} // namespace textlayout
} // namespace skia

View File

@ -84,6 +84,12 @@ public:
void setMaxRunMetrics(const InternalLineMetrics& metrics) { fMaxRunMetrics = metrics; }
InternalLineMetrics getMaxRunMetrics() const { return fMaxRunMetrics; }
bool isFirstLine();
bool isLastLine();
void getRectsForRange(TextRange textRange, RectHeightStyle rectHeightStyle, RectWidthStyle rectWidthStyle, std::vector<TextBox>& boxes);
void getRectsForPlaceholders(std::vector<TextBox>& boxes);
PositionWithAffinity getGlyphPositionAtCoordinate(SkScalar dx);
ClipContext measureTextInsideOneRun(TextRange textRange,
const Run* run,
SkScalar runOffsetInLine,