diff --git a/modules/skparagraph/include/Metrics.h b/modules/skparagraph/include/Metrics.h index d299f15e5a..f754d5b90c 100644 --- a/modules/skparagraph/include/Metrics.h +++ b/modules/skparagraph/include/Metrics.h @@ -69,9 +69,9 @@ public: // descent`. Ascent and descent are provided as positive numbers. Raw numbers // for specific runs of text can be obtained in run_metrics_map. These values // are the cumulative metrics for the entire line. - double fAscent = 0.0; - double fDescent = 0.0; - double fUnscaledAscent = 0.0; + double fAscent = SK_ScalarMax; + double fDescent = SK_ScalarMax; + double fUnscaledAscent = SK_ScalarMax; // Total height of the paragraph including the current line. // // The height of the current line is `round(ascent + descent)`. diff --git a/modules/skparagraph/include/ParagraphStyle.h b/modules/skparagraph/include/ParagraphStyle.h index 871e674895..762e028c01 100644 --- a/modules/skparagraph/include/ParagraphStyle.h +++ b/modules/skparagraph/include/ParagraphStyle.h @@ -45,10 +45,14 @@ struct StrutStyle { bool getHeightOverride() const { return fHeightOverride; } void setHeightOverride(bool v) { fHeightOverride = v; } + void setHalfLeading(bool halfLeading) { fHalfLeading = halfLeading; } + bool getHalfLeading() const { return fHalfLeading; } + bool operator==(const StrutStyle& rhs) const { return this->fEnabled == rhs.fEnabled && this->fHeightOverride == rhs.fHeightOverride && this->fForceHeight == rhs.fForceHeight && + this->fHalfLeading == rhs.fHalfLeading && nearlyEqual(this->fLeading, rhs.fLeading) && nearlyEqual(this->fHeight, rhs.fHeight) && nearlyEqual(this->fFontSize, rhs.fFontSize) && @@ -66,6 +70,9 @@ private: bool fForceHeight; bool fEnabled; bool fHeightOverride; + // true: half leading. + // false: scale ascent/descent with fHeight. + bool fHalfLeading; }; struct ParagraphStyle { diff --git a/modules/skparagraph/include/TextStyle.h b/modules/skparagraph/include/TextStyle.h index ae6e854e01..e5764145ab 100644 --- a/modules/skparagraph/include/TextStyle.h +++ b/modules/skparagraph/include/TextStyle.h @@ -221,6 +221,9 @@ public: void setHeightOverride(bool heightOverride) { fHeightOverride = heightOverride; } bool getHeightOverride() const { return fHeightOverride; } + void setHalfLeading(bool halfLeading) { fHalfLeading = halfLeading; } + bool getHalfLeading() const { return fHalfLeading; } + void setLetterSpacing(SkScalar letterSpacing) { fLetterSpacing = letterSpacing; } SkScalar getLetterSpacing() const { return fLetterSpacing; } @@ -262,6 +265,9 @@ private: SkScalar fFontSize = 14.0; SkScalar fHeight = 1.0; bool fHeightOverride = false; + // true: half leading. + // false: scale ascent/descent with fHeight. + bool fHalfLeading = false; SkString fLocale = {}; SkScalar fLetterSpacing = 0.0; SkScalar fWordSpacing = 0.0; diff --git a/modules/skparagraph/src/OneLineShaper.cpp b/modules/skparagraph/src/OneLineShaper.cpp index be6ae9e5bc..b6a91c5759 100644 --- a/modules/skparagraph/src/OneLineShaper.cpp +++ b/modules/skparagraph/src/OneLineShaper.cpp @@ -144,7 +144,8 @@ void OneLineShaper::fillGaps(size_t startingCount) { } } -void OneLineShaper::finish(TextRange blockText, SkScalar height, SkScalar& advanceX) { +void OneLineShaper::finish(const Block& block, SkScalar height, SkScalar& advanceX) { + auto blockText = block.fRange; // Add all unresolved blocks to resolved blocks while (!fUnresolvedBlocks.empty()) { @@ -165,30 +166,30 @@ void OneLineShaper::finish(TextRange blockText, SkScalar height, SkScalar& advan // Go through all of them size_t lastTextEnd = blockText.start; - for (auto& block : fResolvedBlocks) { + for (auto& resolvedBlock : fResolvedBlocks) { - if (block.fText.end <= blockText.start) { + if (resolvedBlock.fText.end <= blockText.start) { continue; } - if (block.fRun != nullptr) { - fParagraph->fFontSwitches.emplace_back(block.fText.start, block.fRun->fFont); + if (resolvedBlock.fRun != nullptr) { + fParagraph->fFontSwitches.emplace_back(resolvedBlock.fText.start, resolvedBlock.fRun->fFont); } - auto run = block.fRun; - auto glyphs = block.fGlyphs; - auto text = block.fText; + auto run = resolvedBlock.fRun; + auto glyphs = resolvedBlock.fGlyphs; + auto text = resolvedBlock.fText; if (lastTextEnd != text.start) { SkDEBUGF("Text ranges mismatch: ...:%d] - [%d:%d] (%d-%d)\n", lastTextEnd, text.start, text.end, glyphs.start, glyphs.end); SkASSERT(false); } lastTextEnd = text.end; - if (block.isFullyResolved()) { + if (resolvedBlock.isFullyResolved()) { // Just move the entire run - block.fRun->fIndex = this->fParagraph->fRuns.size(); - this->fParagraph->fRuns.emplace_back(*block.fRun); - block.fRun.reset(); + resolvedBlock.fRun->fIndex = this->fParagraph->fRuns.size(); + this->fParagraph->fRuns.emplace_back(*resolvedBlock.fRun); + resolvedBlock.fRun.reset(); continue; } else if (run == nullptr) { continue; @@ -207,6 +208,7 @@ void OneLineShaper::finish(TextRange blockText, SkScalar height, SkScalar& advan info, run->fClusterStart, height, + block.fStyle.getHalfLeading(), this->fParagraph->fRuns.count(), advanceX ); @@ -598,6 +600,7 @@ bool OneLineShaper::iterateThroughShapingRegions(const ShapeVisitor& shape) { runInfo, 0, 0.0f, + false, fParagraph->fRuns.count(), advanceX); @@ -633,6 +636,7 @@ bool OneLineShaper::shape() { // Start from the beginning (hoping that it's a simple case one block - one run) fHeight = block.fStyle.getHeightOverride() ? block.fStyle.getHeight() : 0; + fUseHalfLeading = block.fStyle.getHalfLeading(); fAdvance = SkVector::Make(advanceX, 0); fCurrentText = block.fRange; fUnresolvedBlocks.emplace_back(RunBlock(block.fRange)); @@ -696,7 +700,7 @@ bool OneLineShaper::shape() { } }); - this->finish(block.fRange, fHeight, advanceX); + this->finish(block, fHeight, advanceX); }); return true; diff --git a/modules/skparagraph/src/OneLineShaper.h b/modules/skparagraph/src/OneLineShaper.h index e40bf0ac49..53bb31225c 100644 --- a/modules/skparagraph/src/OneLineShaper.h +++ b/modules/skparagraph/src/OneLineShaper.h @@ -18,6 +18,7 @@ public: explicit OneLineShaper(ParagraphImpl* paragraph) : fParagraph(paragraph) , fHeight(0.0f) + , fUseHalfLeading(false) , fAdvance(SkPoint::Make(0.0f, 0.0f)) , fUnresolvedGlyphs(0) , fUniqueRunId(paragraph->fRuns.size()){ } @@ -69,7 +70,7 @@ private: #ifdef SK_DEBUG void printState(); #endif - void finish(TextRange text, SkScalar height, SkScalar& advanceX); + void finish(const Block& block, SkScalar height, SkScalar& advanceX); void beginLine() override {} void runInfo(const RunInfo&) override {} @@ -81,6 +82,7 @@ private: info, fCurrentText.start, fHeight, + fUseHalfLeading, ++fUniqueRunId, fAdvance.fX); return fCurrentRun->newRunBuffer(); @@ -101,6 +103,7 @@ private: ParagraphImpl* fParagraph; TextRange fCurrentText; SkScalar fHeight; + bool fUseHalfLeading; SkVector fAdvance; size_t fUnresolvedGlyphs; size_t fUniqueRunId; diff --git a/modules/skparagraph/src/ParagraphImpl.cpp b/modules/skparagraph/src/ParagraphImpl.cpp index 42c9ffe475..50cde651fa 100644 --- a/modules/skparagraph/src/ParagraphImpl.cpp +++ b/modules/skparagraph/src/ParagraphImpl.cpp @@ -914,11 +914,20 @@ void ParagraphImpl::computeEmptyMetrics() { fEmptyMetrics.leading()); } else if (!paragraphStyle().getStrutStyle().getForceStrutHeight() && textStyle.getHeightOverride()) { - auto multiplier = textStyle.getHeight() * textStyle.getFontSize() / fEmptyMetrics.height(); - fEmptyMetrics.update( - fEmptyMetrics.ascent() * multiplier, - fEmptyMetrics.descent() * multiplier, - fEmptyMetrics.leading() * multiplier); + const auto intrinsicHeight = fEmptyMetrics.height(); + const auto strutHeight = textStyle.getHeight() * textStyle.getFontSize(); + if (paragraphStyle().getStrutStyle().getHalfLeading()) { + fEmptyMetrics.update( + fEmptyMetrics.ascent(), + fEmptyMetrics.descent(), + fEmptyMetrics.leading() + strutHeight - intrinsicHeight); + } else { + const auto multiplier = strutHeight / intrinsicHeight; + fEmptyMetrics.update( + fEmptyMetrics.ascent() * multiplier, + fEmptyMetrics.descent() * multiplier, + fEmptyMetrics.leading() * multiplier); + } } if (fParagraphStyle.getStrutStyle().getStrutEnabled()) { diff --git a/modules/skparagraph/src/ParagraphStyle.cpp b/modules/skparagraph/src/ParagraphStyle.cpp index 4e3094f472..823f97d65b 100644 --- a/modules/skparagraph/src/ParagraphStyle.cpp +++ b/modules/skparagraph/src/ParagraphStyle.cpp @@ -16,6 +16,7 @@ StrutStyle::StrutStyle() { fLeading = -1; fForceHeight = false; fHeightOverride = false; + fHalfLeading = false; fEnabled = false; } diff --git a/modules/skparagraph/src/Run.cpp b/modules/skparagraph/src/Run.cpp index 39d53d2f24..88f7c2961b 100644 --- a/modules/skparagraph/src/Run.cpp +++ b/modules/skparagraph/src/Run.cpp @@ -18,6 +18,7 @@ Run::Run(ParagraphImpl* owner, const SkShaper::RunHandler::RunInfo& info, size_t firstChar, SkScalar heightMultiplier, + bool useHalfLeading, size_t index, SkScalar offsetX) : fOwner(owner) @@ -26,6 +27,7 @@ Run::Run(ParagraphImpl* owner, , fFont(info.fFont) , fClusterStart(firstChar) , fHeightMultiplier(heightMultiplier) + , fUseHalfLeading(useHalfLeading) { fBidiLevel = info.fBidiLevel; fAdvance = info.fAdvance; @@ -53,9 +55,17 @@ void Run::calculateMetrics() { fCorrectAscent = fFontMetrics.fAscent - fFontMetrics.fLeading * 0.5; fCorrectDescent = fFontMetrics.fDescent + fFontMetrics.fLeading * 0.5; fCorrectLeading = 0; - if (!SkScalarNearlyZero(fHeightMultiplier)) { - auto multiplier = fHeightMultiplier * fFont.getSize() / - (fFontMetrics.fDescent - fFontMetrics.fAscent + fFontMetrics.fLeading); + if (SkScalarNearlyZero(fHeightMultiplier)) { + return; + } + const auto runHeight = fHeightMultiplier * fFont.getSize(); + const auto fontIntrinsicHeight = fCorrectDescent - fCorrectAscent; + if (fUseHalfLeading) { + const auto extraLeading = (runHeight - fontIntrinsicHeight) / 2; + fCorrectAscent -= extraLeading; + fCorrectDescent += extraLeading; + } else { + const auto multiplier = runHeight / fontIntrinsicHeight; fCorrectAscent *= multiplier; fCorrectDescent *= multiplier; } diff --git a/modules/skparagraph/src/Run.h b/modules/skparagraph/src/Run.h index 295cde575c..8a5e41fd3d 100644 --- a/modules/skparagraph/src/Run.h +++ b/modules/skparagraph/src/Run.h @@ -58,6 +58,7 @@ public: const SkShaper::RunHandler::RunInfo& info, size_t firstChar, SkScalar heightMultiplier, + bool useHalfLeading, size_t index, SkScalar shiftX); Run(const Run&) = default; @@ -95,6 +96,7 @@ public: TextDirection getTextDirection() const { return leftToRight() ? TextDirection::kLtr : TextDirection::kRtl; } size_t index() const { return fIndex; } SkScalar heightMultiplier() const { return fHeightMultiplier; } + bool useHalfLeading() const { return fUseHalfLeading; } PlaceholderStyle* placeholderStyle() const; bool isPlaceholder() const { return fPlaceholderIndex != std::numeric_limits::max(); } size_t clusterIndex(size_t pos) const { return fClusterIndexes[pos]; } @@ -190,6 +192,7 @@ private: SkFontMetrics fFontMetrics; const SkScalar fHeightMultiplier; + const bool fUseHalfLeading; SkScalar fCorrectAscent; SkScalar fCorrectDescent; SkScalar fCorrectLeading; @@ -382,7 +385,6 @@ public: } void add(Run* run) { - if (fForceStrut) { return; } @@ -406,11 +408,11 @@ public: } void clean() { - fAscent = 0; - fDescent = 0; + fAscent = SK_ScalarMax; + fDescent = SK_ScalarMin; fLeading = 0; - fRawAscent = 0; - fRawDescent = 0; + fRawAscent = SK_ScalarMax; + fRawDescent = SK_ScalarMin; fRawLeading = 0; } diff --git a/modules/skparagraph/src/TextLine.cpp b/modules/skparagraph/src/TextLine.cpp index c662d8e527..8d0e8ba401 100644 --- a/modules/skparagraph/src/TextLine.cpp +++ b/modules/skparagraph/src/TextLine.cpp @@ -531,8 +531,8 @@ std::unique_ptr TextLine::shapeEllipsis(const SkString& ellipsis, const Run class ShapeHandler final : public SkShaper::RunHandler { public: - ShapeHandler(SkScalar lineHeight, const SkString& ellipsis) - : fRun(nullptr), fLineHeight(lineHeight), fEllipsis(ellipsis) {} + ShapeHandler(SkScalar lineHeight, bool useHalfLeading, const SkString& ellipsis) + : fRun(nullptr), fLineHeight(lineHeight), fUseHalfLeading(useHalfLeading), fEllipsis(ellipsis) {} Run* run() & { return fRun.get(); } std::unique_ptr run() && { return std::move(fRun); } @@ -545,7 +545,7 @@ std::unique_ptr TextLine::shapeEllipsis(const SkString& ellipsis, const Run Buffer runBuffer(const RunInfo& info) override { SkASSERT(!fRun); - fRun = std::make_unique(nullptr, info, 0, fLineHeight, 0, 0); + fRun = std::make_unique(nullptr, info, 0, fLineHeight, fUseHalfLeading, 0, 0); return fRun->newRunBuffer(); } @@ -560,10 +560,11 @@ std::unique_ptr TextLine::shapeEllipsis(const SkString& ellipsis, const Run std::unique_ptr fRun; SkScalar fLineHeight; + bool fUseHalfLeading; SkString fEllipsis; }; - ShapeHandler handler(run.heightMultiplier(), ellipsis); + ShapeHandler handler(run.heightMultiplier(), run.useHalfLeading(), ellipsis); std::unique_ptr shaper = SkShaper::MakeShapeDontWrapOrReorder(); SkASSERT_RELEASE(shaper != nullptr); shaper->shape(ellipsis.c_str(), ellipsis.size(), run.font(), true, @@ -1010,13 +1011,12 @@ void TextLine::getRectsForRange(TextRange textRange0, } 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 - auto correctedHeight = clip.height() / run->fHeightMultiplier; - auto verticalShift = this->sizes().runTop(context.run, LineMetricStyle::Typographic); - clip.fTop += verticalShift; - clip.fBottom = clip.fTop + correctedHeight; + if (run->fHeightMultiplier <= 0) { + break; } + const auto effectiveBaseline = this->baseline() + this->sizes().delta(); + clip.fTop = effectiveBaseline + run->ascent(); + clip.fBottom = effectiveBaseline + run->descent(); } break; default: diff --git a/modules/skparagraph/src/TextStyle.cpp b/modules/skparagraph/src/TextStyle.cpp index afb4c37366..49c2f3b117 100644 --- a/modules/skparagraph/src/TextStyle.cpp +++ b/modules/skparagraph/src/TextStyle.cpp @@ -20,6 +20,7 @@ TextStyle::TextStyle(const TextStyle& other, bool placeholder) { fHeightOverride = other.fHeightOverride; fIsPlaceholder = placeholder; fFontFeatures = other.fFontFeatures; + fHalfLeading = other.fHalfLeading; } bool TextStyle::equals(const TextStyle& other) const { @@ -49,6 +50,9 @@ bool TextStyle::equals(const TextStyle& other) const { if (fHeight != other.fHeight) { return false; } + if (fHalfLeading != other.fHalfLeading) { + return false; + } if (fFontSize != other.fFontSize) { return false; } @@ -134,7 +138,8 @@ bool TextStyle::matchOneAttribute(StyleType styleType, const TextStyle& other) c fLocale == other.fLocale && fFontFamilies == other.fFontFamilies && fFontSize == other.fFontSize && - fHeight == other.fHeight; + fHeight == other.fHeight && + fHalfLeading == other.fHalfLeading; default: SkASSERT(false); return false; diff --git a/modules/skparagraph/tests/SkParagraphTest.cpp b/modules/skparagraph/tests/SkParagraphTest.cpp index ac7b356f3c..f8961eaa13 100644 --- a/modules/skparagraph/tests/SkParagraphTest.cpp +++ b/modules/skparagraph/tests/SkParagraphTest.cpp @@ -1267,6 +1267,293 @@ DEF_TEST(SkParagraph_HeightOverrideParagraph, reporter) { REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.bottom(), 165.495f, EPSILON5)); } +DEF_TEST(SkParagraph_BasicHalfLeading, reporter) { + sk_sp fontCollection = sk_make_sp(); + + if (!fontCollection->fontsFound()) { + return; + } + + const char* text = "01234満毎冠行来昼本可\nabcd\n満毎冠行来昼本可"; + const size_t len = strlen(text); + + TestCanvas canvas("SkParagraph_BasicHalfLeading.png"); + + ParagraphStyle paragraph_style; + TextStyle text_style; + text_style.setFontFamilies({SkString("Roboto")}); + text_style.setFontSize(20.0f); + text_style.setColor(SK_ColorBLACK); + text_style.setLetterSpacing(0.0f); + text_style.setWordSpacing(0.0f); + text_style.setHeightOverride(true); + text_style.setHeight(3.6345f); + text_style.setHalfLeading(true); + + ParagraphBuilderImpl builder(paragraph_style, fontCollection); + + builder.pushStyle(text_style); + builder.addText(text); + + auto paragraph = builder.Build(); + paragraph->layout(550); + + auto impl = static_cast(paragraph.get()); + REPORTER_ASSERT(reporter, impl->styles().size() == 1); // paragraph style does not count + REPORTER_ASSERT(reporter, impl->styles()[0].fStyle.equals(text_style)); + + paragraph->paint(canvas.get(), 0, 0); + + const RectWidthStyle rect_width_style = RectWidthStyle::kTight; + std::vector boxes = paragraph->getRectsForRange(0, len, RectHeightStyle::kTight, rect_width_style); + std::vector lineBoxes = paragraph->getRectsForRange(0, len, RectHeightStyle::kMax, rect_width_style); + + canvas.drawRects(SK_ColorBLUE, boxes); + REPORTER_ASSERT(reporter, boxes.size() == 3ull); + REPORTER_ASSERT(reporter, lineBoxes.size() == boxes.size()); + + const auto line_spacing1 = boxes[1].rect.top() - boxes[0].rect.bottom(); + const auto line_spacing2 = boxes[2].rect.top() - boxes[1].rect.bottom(); + + // Uniform line spacing. + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(line_spacing1, line_spacing2)); + + // line spacing is distributed evenly over and under the text. + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lineBoxes[0].rect.bottom() - boxes[0].rect.bottom(), boxes[0].rect.top() - lineBoxes[0].rect.top())); + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lineBoxes[1].rect.bottom() - boxes[1].rect.bottom(), boxes[1].rect.top() - lineBoxes[1].rect.top())); + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lineBoxes[2].rect.bottom() - boxes[2].rect.bottom(), boxes[2].rect.top() - lineBoxes[2].rect.top())); + + // Half leading does not move the text horizontally. + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.left(), 0, EPSILON100)); + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.right(), 43.843f, EPSILON100)); +} + +DEF_TEST(SkParagraph_NearZeroHeightMixedDistribution, reporter) { + sk_sp fontCollection = sk_make_sp(); + + if (!fontCollection->fontsFound()) { + return; + } + + const char* text = "Cookies need love"; + const size_t len = strlen(text); + + TestCanvas canvas("SkParagraph_ZeroHeightHalfLeading.png"); + + ParagraphStyle paragraph_style; + paragraph_style.setTextHeightBehavior(TextHeightBehavior::kAll); + TextStyle text_style; + text_style.setFontFamilies({SkString("Roboto")}); + text_style.setFontSize(20.0f); + text_style.setColor(SK_ColorBLACK); + text_style.setLetterSpacing(0.0f); + text_style.setWordSpacing(0.0f); + text_style.setHeightOverride(true); + text_style.setHeight(0.001f); + + ParagraphBuilderImpl builder(paragraph_style, fontCollection); + + // First run, half leading. + text_style.setHalfLeading(true); + builder.pushStyle(text_style); + builder.addText(text); + + // Second run, no half leading. + text_style.setHalfLeading(false); + builder.pushStyle(text_style); + builder.addText(text); + + auto paragraph = builder.Build(); + paragraph->layout(550); + paragraph->paint(canvas.get(), 0, 0); + + auto impl = static_cast(paragraph.get()); + REPORTER_ASSERT(reporter, impl->runs().size() == 2); + REPORTER_ASSERT(reporter, impl->styles().size() == 2); // paragraph style does not count + REPORTER_ASSERT(reporter, impl->lines().size() == 1ull); + + const RectWidthStyle rect_width_style = RectWidthStyle::kTight; + + std::vector boxes = paragraph->getRectsForRange(0, len, RectHeightStyle::kTight, rect_width_style); + std::vector lineBoxes = paragraph->getRectsForRange(0, len, RectHeightStyle::kMax, rect_width_style); + + canvas.drawRects(SK_ColorBLUE, boxes); + REPORTER_ASSERT(reporter, boxes.size() == 1ull); + REPORTER_ASSERT(reporter, lineBoxes.size() == boxes.size()); + + // From font metrics. + const auto metricsAscent = -18.5546875f; + const auto metricsDescent = 4.8828125f; + + // As the height multiplier converges to 0 (but not 0 since 0 is used as a + // magic value to indicate there's no height multiplier), the `Run`s top + // edge and bottom edge will converge to a horizontal line: + // - When half leading is used the vertical line is roughly the center of + // of the glyphs in the run ((fontMetrics.descent - fontMetrics.ascent) / 2) + // - When half leading is disabled the line is the alphabetic baseline. + + // Expected values in baseline coordinate space: + const auto run1_ascent = (metricsAscent + metricsDescent) / 2; + const auto run1_descent = (metricsAscent + metricsDescent) / 2; + const auto run2_ascent = 0.0f; + const auto run2_descent = 0.0f; + const auto line_top = std::min(run1_ascent, run2_ascent); + const auto line_bottom = std::max(run1_descent, run2_descent); + + // Expected glyph height in linebox coordinate space: + const auto glyphs_top = metricsAscent - line_top; + const auto glyphs_bottom = metricsDescent - line_top; + + // kTight reports the glyphs' bounding box in the linebox's coordinate + // space. + const auto actual_glyphs_top = boxes[0].rect.top() - lineBoxes[0].rect.top(); + const auto actual_glyphs_bottom = boxes[0].rect.bottom() - lineBoxes[0].rect.top(); + + // Use a relatively large epsilon since the heightMultiplier is not actually + // 0. + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(glyphs_top, actual_glyphs_top, EPSILON20)); + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(glyphs_bottom, actual_glyphs_bottom, EPSILON20)); + + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lineBoxes[0].rect.height(), line_bottom - line_top, EPSILON2)); + REPORTER_ASSERT(reporter, lineBoxes[0].rect.height() > 1); + + // Half leading does not move the text horizontally. + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 0, EPSILON100)); +} + +DEF_TEST(SkParagraph_StrutHalfLeading, reporter) { + sk_sp fontCollection = sk_make_sp(); + + if (!fontCollection->fontsFound()) { + return; + } + + const char* text = "01234満毎冠行来昼本可\nabcd\n満毎冠行来昼本可"; + const size_t len = strlen(text); + + TestCanvas canvas("SkParagraph_StrutHalfLeading.png"); + + ParagraphStyle paragraph_style; + // Tiny font and height multiplier to ensure the height is entirely decided + // by the strut. + TextStyle text_style; + text_style.setFontFamilies({SkString("Roboto")}); + text_style.setFontSize(1.0f); + text_style.setColor(SK_ColorBLACK); + text_style.setLetterSpacing(0.0f); + text_style.setWordSpacing(0.0f); + text_style.setHeight(0.1f); + + StrutStyle strut_style; + strut_style.setFontFamilies({SkString("Roboto")}); + strut_style.setFontSize(20.0f); + strut_style.setHeight(3.6345f); + strut_style.setHalfLeading(true); + strut_style.setStrutEnabled(true); + strut_style.setForceStrutHeight(true); + + ParagraphBuilderImpl builder(paragraph_style, fontCollection); + + builder.pushStyle(text_style); + builder.addText(text); + + auto paragraph = builder.Build(); + paragraph->layout(550); + + auto impl = static_cast(paragraph.get()); + REPORTER_ASSERT(reporter, impl->styles().size() == 1); // paragraph style does not count + + paragraph->paint(canvas.get(), 0, 0); + + const RectWidthStyle rect_width_style = RectWidthStyle::kTight; + std::vector boxes = paragraph->getRectsForRange(0, len, RectHeightStyle::kTight, rect_width_style); + std::vector lineBoxes = paragraph->getRectsForRange(0, len, RectHeightStyle::kMax, rect_width_style); + + canvas.drawRects(SK_ColorBLUE, boxes); + REPORTER_ASSERT(reporter, boxes.size() == 3ull); + REPORTER_ASSERT(reporter, lineBoxes.size() == boxes.size()); + + const auto line_spacing1 = boxes[1].rect.top() - boxes[0].rect.bottom(); + const auto line_spacing2 = boxes[2].rect.top() - boxes[1].rect.bottom(); + + // Uniform line spacing. + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(line_spacing1, line_spacing2)); + + // line spacing is distributed evenly over and under the text. + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lineBoxes[0].rect.bottom() - boxes[0].rect.bottom(), boxes[0].rect.top() - lineBoxes[0].rect.top())); + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lineBoxes[1].rect.bottom() - boxes[1].rect.bottom(), boxes[1].rect.top() - lineBoxes[1].rect.top())); + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lineBoxes[2].rect.bottom() - boxes[2].rect.bottom(), boxes[2].rect.top() - lineBoxes[2].rect.top())); + + // Half leading does not move the text horizontally. + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.left(), 0, EPSILON100)); +} + +DEF_TEST(SkParagraph_TrimLeadingDistribution, reporter) { + sk_sp fontCollection = sk_make_sp(); + + if (!fontCollection->fontsFound()) { + return; + } + + const char* text = "01234満毎冠行来昼本可\nabcd\n満毎冠行来昼本可"; + const size_t len = strlen(text); + + TestCanvas canvas("SkParagraph_TrimHalfLeading.png"); + + ParagraphStyle paragraph_style; + paragraph_style.setTextHeightBehavior(TextHeightBehavior::kDisableAll); + TextStyle text_style; + text_style.setFontFamilies({SkString("Roboto")}); + text_style.setFontSize(20.0f); + text_style.setColor(SK_ColorBLACK); + text_style.setLetterSpacing(0.0f); + text_style.setWordSpacing(0.0f); + text_style.setHeightOverride(true); + text_style.setHeight(3.6345f); + text_style.setHalfLeading(true); + + ParagraphBuilderImpl builder(paragraph_style, fontCollection); + + builder.pushStyle(text_style); + builder.addText(text); + + auto paragraph = builder.Build(); + paragraph->layout(550); + paragraph->paint(canvas.get(), 0, 0); + + const RectWidthStyle rect_width_style = RectWidthStyle::kTight; + + std::vector boxes = paragraph->getRectsForRange(0, len, RectHeightStyle::kTight, rect_width_style); + std::vector lineBoxes = paragraph->getRectsForRange(0, len, RectHeightStyle::kMax, rect_width_style); + + canvas.drawRects(SK_ColorBLUE, boxes); + REPORTER_ASSERT(reporter, boxes.size() == 3ull); + REPORTER_ASSERT(reporter, lineBoxes.size() == boxes.size()); + + const auto line_spacing1 = boxes[1].rect.top() - boxes[0].rect.bottom(); + const auto line_spacing2 = boxes[2].rect.top() - boxes[1].rect.bottom(); + + // Uniform line spacing. The delta is introduced by the height rounding. + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(line_spacing1, line_spacing2, 1)); + + // Trim the first line's top leading. + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lineBoxes[0].rect.top(), boxes[0].rect.top())); + // Trim the last line's bottom leading. + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lineBoxes[2].rect.bottom(), boxes[2].rect.bottom())); + + const auto halfLeading = lineBoxes[0].rect.bottom() - boxes[0].rect.bottom(); + // Large epsilon because of rounding. + const auto epsilon = EPSILON10; + // line spacing is distributed evenly over and under the text. + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.top() - lineBoxes[1].rect.top(), halfLeading, epsilon)); + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lineBoxes[1].rect.bottom() - boxes[1].rect.bottom(), halfLeading)); + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[2].rect.top() - lineBoxes[2].rect.top(), halfLeading, epsilon)); + + // Half leading does not move the text horizontally. + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.left(), 0, EPSILON100)); + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.right(), 43.843f, EPSILON100)); +} + DEF_TEST(SkParagraph_LeftAlignParagraph, reporter) { sk_sp fontCollection = sk_make_sp(); if (!fontCollection->fontsFound()) return;