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:
parent
b32d66b296
commit
8335ab694d
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user