diff --git a/engine/src/flutter/lib/ui/text/paragraph_builder.cc b/engine/src/flutter/lib/ui/text/paragraph_builder.cc index 712b242d75..41991502c1 100644 --- a/engine/src/flutter/lib/ui/text/paragraph_builder.cc +++ b/engine/src/flutter/lib/ui/text/paragraph_builder.cc @@ -108,7 +108,7 @@ PassRefPtr decodeParagraphStyle(RenderStyle* parentStyle, const std::string& fontFamily, double fontSize, double lineHeight, - const std::string& ellipsis) { + const std::u16string& ellipsis) { FTL_DCHECK(encoded.num_elements() == 5); RefPtr style = RenderStyle::create(); @@ -155,8 +155,10 @@ PassRefPtr decodeParagraphStyle(RenderStyle* parentStyle, if (mask & psMaxLinesMask) style->setMaxLines(encoded[psMaxLinesIndex]); - if (mask & psEllipsisMask) - style->setEllipsis(AtomicString::fromUTF8(ellipsis.c_str())); + if (mask & psEllipsisMask) { + style->setEllipsis( + AtomicString(reinterpret_cast(ellipsis.c_str()))); + } return style.release(); } @@ -193,7 +195,7 @@ ftl::RefPtr ParagraphBuilder::create( const std::string& fontFamily, double fontSize, double lineHeight, - const std::string& ellipsis) { + const std::u16string& ellipsis) { return ftl::MakeRefCounted(encoded, fontFamily, fontSize, lineHeight, ellipsis); } @@ -202,7 +204,7 @@ ParagraphBuilder::ParagraphBuilder(tonic::Int32List& encoded, const std::string& fontFamily, double fontSize, double lineHeight, - const std::string& ellipsis) { + const std::u16string& ellipsis) { if (!Settings::Get().using_blink) { int32_t mask = encoded[0]; txt::ParagraphStyle style; @@ -232,8 +234,9 @@ ParagraphBuilder::ParagraphBuilder(tonic::Int32List& encoded, if (mask & psMaxLinesMask) style.max_lines = encoded[psMaxLinesIndex]; - if (mask & psEllipsisMask) + if (mask & psEllipsisMask) { style.ellipsis = ellipsis; + } m_paragraphBuilder = std::make_unique( style, blink::FontCollection::ForProcess().GetFontCollection()); diff --git a/engine/src/flutter/lib/ui/text/paragraph_builder.h b/engine/src/flutter/lib/ui/text/paragraph_builder.h index 46aab9beb2..19dca760d7 100644 --- a/engine/src/flutter/lib/ui/text/paragraph_builder.h +++ b/engine/src/flutter/lib/ui/text/paragraph_builder.h @@ -31,7 +31,7 @@ class ParagraphBuilder : public ftl::RefCountedThreadSafe, const std::string& fontFamily, double fontSize, double lineHeight, - const std::string& ellipsis); + const std::u16string& ellipsis); ~ParagraphBuilder() override; @@ -55,7 +55,7 @@ class ParagraphBuilder : public ftl::RefCountedThreadSafe, const std::string& fontFamily, double fontSize, double lineHeight, - const std::string& ellipsis); + const std::u16string& ellipsis); void createRenderView(); diff --git a/engine/src/flutter/third_party/txt/src/txt/paragraph.cc b/engine/src/flutter/third_party/txt/src/txt/paragraph.cc index 010e90225d..b7082ac33d 100644 --- a/engine/src/flutter/third_party/txt/src/txt/paragraph.cc +++ b/engine/src/flutter/third_party/txt/src/txt/paragraph.cc @@ -254,6 +254,7 @@ void Paragraph::Layout(double width, bool force) { std::vector buffers; std::vector buffer_sizes; int word_count = 0; + size_t max_lines = paragraph_style_.max_lines; auto postprocess_line = [this, &x_queue, &y]() -> void { size_t record_index = 0; @@ -326,22 +327,68 @@ void Paragraph::Layout(double width, bool force) { size_t layout_start = run.start; // Layout until the end of the run or too many lines. - while (layout_start < run.end && lines_ < paragraph_style_.max_lines) { + while (layout_start < run.end && lines_ < max_lines) { const size_t next_break = (break_index > breaks_count - 1) ? std::numeric_limits::max() : breaks[break_index]; const size_t layout_end = std::min(run.end, next_break); bool bidiFlags = paragraph_style_.rtl; + std::shared_ptr minikin_font_collection = + font_collection_->GetMinikinFontCollectionForFamily( + run.style.font_family); + + uint16_t* text_ptr = text.data() + layout_start; + size_t text_count = layout_end - layout_start; + std::vector ellipsized_text; + + // Apply ellipsizing if the run was not completely laid out and this + // is the last line (or lines are unlimited). + const std::u16string& ellipsis = paragraph_style_.ellipsis; + if (ellipsis.length() && !isinf(width_) && run.end != layout_end && + (lines_ == max_lines - 1 || + max_lines == std::numeric_limits::max())) { + float ellipsis_width = layout.measureText( + reinterpret_cast(ellipsis.data()), + 0, ellipsis.length(), ellipsis.length(), bidiFlags, + font, minikin_paint, minikin_font_collection, nullptr); + + std::vector text_advances(text_count); + float text_width = layout.measureText( + text.data() + layout_start, 0, text_count, text_count, + bidiFlags, font, minikin_paint, minikin_font_collection, + text_advances.data()); + + // Truncate characters from the text until the ellipsis fits. + size_t truncate_count = 0; + while (truncate_count < text_count && + text_width + ellipsis_width > width_) { + text_width -= text_advances[text_count - truncate_count - 1]; + truncate_count++; + } + + ellipsized_text.reserve(text_count - truncate_count + ellipsis.length()); + ellipsized_text.insert(ellipsized_text.begin(), + text.begin() + layout_start, + text.begin() + layout_end - truncate_count); + ellipsized_text.insert(ellipsized_text.end(), + ellipsis.begin(), ellipsis.end()); + text_ptr = ellipsized_text.data(); + text_count = ellipsized_text.size(); + + // If there is no line limit, then skip all lines after the ellipsized + // line. + if (max_lines == std::numeric_limits::max()) + max_lines = lines_ + 1; + } + // Minikin Layout doLayout() has an O(N^2) (according to // benchmarks) time complexity where N is the total number of characters. // However, this is not significant for reasonably sized paragraphs. It is // currently recommended to break up very long paragraphs (10k+ // characters) to ensure speedy layout. - layout.doLayout(text.data() + layout_start, 0, layout_end - layout_start, - layout_end - layout_start, bidiFlags, font, minikin_paint, - font_collection_->GetMinikinFontCollectionForFamily( - run.style.font_family)); + layout.doLayout(text_ptr, 0, text_count, text_count, + bidiFlags, font, minikin_paint, minikin_font_collection); FillWhitespaceSet(layout_start, layout_end, minikin::getHbFontLocked(layout.getFont(0))); diff --git a/engine/src/flutter/third_party/txt/src/txt/paragraph.h b/engine/src/flutter/third_party/txt/src/txt/paragraph.h index 381a509295..04e1b3a252 100644 --- a/engine/src/flutter/third_party/txt/src/txt/paragraph.h +++ b/engine/src/flutter/third_party/txt/src/txt/paragraph.h @@ -164,6 +164,7 @@ class Paragraph { FRIEND_TEST(ParagraphTest, EmojiParagraph); FRIEND_TEST(ParagraphTest, HyphenBreakParagraph); FRIEND_TEST(ParagraphTest, RepeatLayoutParagraph); + FRIEND_TEST(ParagraphTest, Ellipsize); // Starting data to layout. std::vector text_; diff --git a/engine/src/flutter/third_party/txt/src/txt/paragraph_style.h b/engine/src/flutter/third_party/txt/src/txt/paragraph_style.h index 634e05b9a8..1eebb9e73b 100644 --- a/engine/src/flutter/third_party/txt/src/txt/paragraph_style.h +++ b/engine/src/flutter/third_party/txt/src/txt/paragraph_style.h @@ -35,9 +35,9 @@ class ParagraphStyle { double font_size = 14; TextAlign text_align = TextAlign::left; - size_t max_lines = UINT_MAX; + size_t max_lines = std::numeric_limits::max(); double line_height = 1.0; - std::string ellipsis = "..."; + std::u16string ellipsis; // Default strategy is kBreakStrategy_Greedy. Sometimes, // kBreakStrategy_HighQuality will produce more desireable layouts (eg, very // long words are more likely to be reasonably placed). diff --git a/engine/src/flutter/third_party/txt/src/txt/styled_runs.h b/engine/src/flutter/third_party/txt/src/txt/styled_runs.h index d80de37240..f277f2913e 100644 --- a/engine/src/flutter/third_party/txt/src/txt/styled_runs.h +++ b/engine/src/flutter/third_party/txt/src/txt/styled_runs.h @@ -81,6 +81,7 @@ class StyledRuns { FRIEND_TEST(ParagraphTest, KernParagraph); FRIEND_TEST(ParagraphTest, HyphenBreakParagraph); FRIEND_TEST(ParagraphTest, RepeatLayoutParagraph); + FRIEND_TEST(ParagraphTest, Ellipsize); struct IndexedRun { size_t style_index = 0; diff --git a/engine/src/flutter/third_party/txt/tests/paragraph_unittests.cc b/engine/src/flutter/third_party/txt/tests/paragraph_unittests.cc index ab13ff5b12..5b47d1b422 100644 --- a/engine/src/flutter/third_party/txt/tests/paragraph_unittests.cc +++ b/engine/src/flutter/third_party/txt/tests/paragraph_unittests.cc @@ -1522,4 +1522,36 @@ TEST_F(ParagraphTest, RepeatLayoutParagraph) { ASSERT_TRUE(Snapshot()); } +TEST_F(ParagraphTest, Ellipsize) { + const char* text = + "This is a very long sentence to test if the text will properly wrap " + "around and go to the next line. Sometimes, short sentence. Longer " + "sentences are okay too because they are nessecary. Very short. "; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + paragraph_style.ellipsis = u"\u2026"; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.color = SK_ColorBLACK; + builder.PushStyle(text_style); + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth()); + + paragraph->Paint(GetCanvas(), 0, 0); + + ASSERT_TRUE(Snapshot()); + + // Check that the ellipsizer limited the text to one line and did not wrap + // to a second line. + ASSERT_EQ(paragraph->records_.size(), 1ull); +} + } // namespace txt