diff --git a/lib/ui/text.dart b/lib/ui/text.dart index 74fdcd3de8539..61d4d6afe9c12 100644 --- a/lib/ui/text.dart +++ b/lib/ui/text.dart @@ -497,7 +497,7 @@ class TextStyle { // This encoding must match the C++ version ParagraphBuilder::build. // -// The encoded array buffer has 5 elements. +// The encoded array buffer has 6 elements. // // - Element 0: A bit mask indicating which fields are non-null. // Bit 0 is unused. Bits 1-n are set if the corresponding index in the @@ -506,21 +506,24 @@ class TextStyle { // // - Element 1: The enum index of the |textAlign|. // -// - Element 2: The index of the |fontWeight|. +// - Element 2: The enum index of the |textDirection|. // -// - Element 3: The enum index of the |fontStyle|. +// - Element 3: The index of the |fontWeight|. // -// - Element 4: The value of |maxLines|. +// - Element 4: The enum index of the |fontStyle|. +// +// - Element 5: The value of |maxLines|. // Int32List _encodeParagraphStyle( TextAlign textAlign, TextDirection textDirection, - FontWeight fontWeight, - FontStyle fontStyle, int maxLines, String fontFamily, double fontSize, - double lineHeight, + double height, + FontWeight fontWeight, + FontStyle fontStyle, + StrutStyle strutStyle, String ellipsis, Locale locale, ) { @@ -553,18 +556,22 @@ Int32List _encodeParagraphStyle( result[0] |= 1 << 7; // Passed separately to native. } - if (lineHeight != null) { + if (height != null) { result[0] |= 1 << 8; // Passed separately to native. } - if (ellipsis != null) { + if (strutStyle != null) { result[0] |= 1 << 9; // Passed separately to native. } - if (locale != null) { + if (ellipsis != null) { result[0] |= 1 << 10; // Passed separately to native. } + if (locale != null) { + result[0] |= 1 << 11; + // Passed separately to native. + } return result; } @@ -584,12 +591,6 @@ class ParagraphStyle { /// directionality of the paragraph, as well as the meaning of /// [TextAlign.start] and [TextAlign.end] in the `textAlign` field. /// - /// * `fontWeight`: The typeface thickness to use when painting the text - /// (e.g., bold). - /// - /// * `fontStyle`: The typeface variant to use when drawing the letters (e.g., - /// italics). - /// /// * `maxLines`: The maximum number of lines painted. Lines beyond this /// number are silently dropped. For example, if `maxLines` is 1, then only /// one line is rendered. If `maxLines` is null, but `ellipsis` is not null, @@ -603,8 +604,23 @@ class ParagraphStyle { /// * `fontSize`: The size of glyphs (in logical pixels) to use when painting /// the text. /// - /// * `lineHeight`: The minimum height of the line boxes, as a multiple of the - /// font size. + /// * `height`: The minimum height of the line boxes, as a multiple of the + /// font size. The lines of the paragraph will be at least + /// `(height + leading) * fontSize` tall when fontSize + /// is not null. When fontSize is null, there is no minimum line height. Tall + /// glyphs due to baseline alignment or large [TextStyle.fontSize] may cause + /// the actual line height after layout to be taller than specified here. + /// [fontSize] must be provided for this property to take effect. + /// + /// * `fontWeight`: The typeface thickness to use when painting the text + /// (e.g., bold). + /// + /// * `fontStyle`: The typeface variant to use when drawing the letters (e.g., + /// italics). + /// + /// * `strutStyle`: The properties of the strut. Strut defines a set of minimum + /// vertical line height related metrics and can be used to obtain more + /// advanced line spacing behavior. /// /// * `ellipsis`: String used to ellipsize overflowing text. If `maxLines` is /// not null, then the `ellipsis`, if any, is applied to the last rendered @@ -619,36 +635,40 @@ class ParagraphStyle { ParagraphStyle({ TextAlign textAlign, TextDirection textDirection, - FontWeight fontWeight, - FontStyle fontStyle, int maxLines, String fontFamily, double fontSize, - double lineHeight, + double height, + FontWeight fontWeight, + FontStyle fontStyle, + StrutStyle strutStyle, String ellipsis, Locale locale, }) : _encoded = _encodeParagraphStyle( textAlign, textDirection, - fontWeight, - fontStyle, maxLines, fontFamily, fontSize, - lineHeight, + height, + fontWeight, + fontStyle, + strutStyle, ellipsis, locale, ), _fontFamily = fontFamily, _fontSize = fontSize, - _lineHeight = lineHeight, + _height = height, + _strutStyle = strutStyle, _ellipsis = ellipsis, _locale = locale; final Int32List _encoded; final String _fontFamily; final double _fontSize; - final double _lineHeight; + final double _height; + final StrutStyle _strutStyle; final String _ellipsis; final Locale _locale; @@ -661,7 +681,8 @@ class ParagraphStyle { final ParagraphStyle typedOther = other; if (_fontFamily != typedOther._fontFamily || _fontSize != typedOther._fontSize || - _lineHeight != typedOther._lineHeight || + _height != typedOther._height || + _strutStyle != typedOther._strutStyle || _ellipsis != typedOther._ellipsis || _locale != typedOther._locale) return false; @@ -673,7 +694,7 @@ class ParagraphStyle { } @override - int get hashCode => hashValues(hashList(_encoded), _fontFamily, _fontSize, _lineHeight, _ellipsis, _locale); + int get hashCode => hashValues(hashList(_encoded), _fontFamily, _fontSize, _height, _ellipsis, _locale); @override String toString() { @@ -685,13 +706,172 @@ class ParagraphStyle { 'maxLines: ${ _encoded[0] & 0x020 == 0x020 ? _encoded[5] : "unspecified"}, ' 'fontFamily: ${ _encoded[0] & 0x040 == 0x040 ? _fontFamily : "unspecified"}, ' 'fontSize: ${ _encoded[0] & 0x080 == 0x080 ? _fontSize : "unspecified"}, ' - 'lineHeight: ${ _encoded[0] & 0x100 == 0x100 ? "${_lineHeight}x" : "unspecified"}, ' + 'height: ${ _encoded[0] & 0x100 == 0x100 ? "${_height}x" : "unspecified"}, ' 'ellipsis: ${ _encoded[0] & 0x200 == 0x200 ? "\"$_ellipsis\"" : "unspecified"}, ' 'locale: ${ _encoded[0] & 0x400 == 0x400 ? _locale : "unspecified"}' ')'; } } +// Serialize strut properties into ByteData. This encoding errs towards +// compactness. The first 8 bits is a bitmask that records which properties +// are null. The rest of the values are encoded in the same order encountered +// in the bitmask. The final returned value truncates any unused bytes +// at the end. +// +// We serialize this more thoroughly than ParagraphStyle because it is +// much more likely that the strut is empty/null and we wish to add +// minimal overhead for non-strut cases. +ByteData _encodeStrut( + String fontFamily, + List fontFamilyFallback, + double fontSize, + double height, + double leading, + FontWeight fontWeight, + FontStyle fontStyle, + bool forceStrutHeight) { + if (fontFamily == null && + fontSize == null && + height == null && + leading == null && + fontWeight == null && + fontStyle == null && + forceStrutHeight == null) + return ByteData(0); + + final ByteData data = ByteData(15); // Max size is 15 bytes + int bitmask = 0; // 8 bit mask + int byteCount = 1; + if (fontWeight != null) { + bitmask |= 1 << 0; + data.setInt8(byteCount, fontWeight.index); + byteCount += 1; + } + if (fontStyle != null) { + bitmask |= 1 << 1; + data.setInt8(byteCount, fontStyle.index); + byteCount += 1; + } + if (fontFamily != null || (fontFamilyFallback != null && fontFamilyFallback.isNotEmpty)){ + bitmask |= 1 << 2; + // passed separately to native + } + if (fontSize != null) { + bitmask |= 1 << 3; + data.setFloat32(byteCount, fontSize, _kFakeHostEndian); + byteCount += 4; + } + if (height != null) { + bitmask |= 1 << 4; + data.setFloat32(byteCount, height, _kFakeHostEndian); + byteCount += 4; + } + if (leading != null) { + bitmask |= 1 << 5; + data.setFloat32(byteCount, leading, _kFakeHostEndian); + byteCount += 4; + } + if (forceStrutHeight != null) { + bitmask |= 1 << 6; + // We store this boolean directly in the bitmask since there is + // extra space in the 16 bit int. + bitmask |= (forceStrutHeight ? 1 : 0) << 7; + } + + data.setInt8(0, bitmask); + + return ByteData.view(data.buffer, 0, byteCount); +} + +class StrutStyle { + /// Creates a new StrutStyle object. + /// + /// * `fontFamily`: The name of the font to use when painting the text (e.g., + /// Roboto). + /// + /// * `fontFamilyFallback`: An ordered list of font family names that will be searched for when + /// the font in `fontFamily` cannot be found. + /// + /// * `fontSize`: The size of glyphs (in logical pixels) to use when painting + /// the text. + /// + /// * `lineHeight`: The minimum height of the line boxes, as a multiple of the + /// font size. The lines of the paragraph will be at least + /// `(lineHeight + leading) * fontSize` tall when fontSize + /// is not null. When fontSize is null, there is no minimum line height. Tall + /// glyphs due to baseline alignment or large [TextStyle.fontSize] may cause + /// the actual line height after layout to be taller than specified here. + /// [fontSize] must be provided for this property to take effect. + /// + /// * `leading`: The minimum amount of leading between lines as a multiple of + /// the font size. [fontSize] must be provided for this property to take effect. + /// + /// * `fontWeight`: The typeface thickness to use when painting the text + /// (e.g., bold). + /// + /// * `fontStyle`: The typeface variant to use when drawing the letters (e.g., + /// italics). + /// + /// * `forceStrutHeight`: When true, the paragraph will force all lines to be exactly + /// `(lineHeight + leading) * fontSize` tall from baseline to baseline. + /// [TextStyle] is no longer able to influence the line height, and any tall + /// glyphs may overlap with lines above. If a [fontFamily] is specified, the + /// total ascent of the first line will be the min of the `Ascent + half-leading` + /// of the [fontFamily] and `(lineHeight + leading) * fontSize`. Otherwise, it + /// will be determined by the Ascent + half-leading of the first text. + StrutStyle({ + String fontFamily, + List fontFamilyFallback, + double fontSize, + double height, + double leading, + FontWeight fontWeight, + FontStyle fontStyle, + bool forceStrutHeight, + }) : _encoded = _encodeStrut( + fontFamily, + fontFamilyFallback, + fontSize, + height, + leading, + fontWeight, + fontStyle, + forceStrutHeight, + ), + _fontFamily = fontFamily, + _fontFamilyFallback = fontFamilyFallback; + + final ByteData _encoded; // Most of the data for strut is encoded. + final String _fontFamily; + final List _fontFamilyFallback; + + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + final StrutStyle typedOther = other; + if (_fontFamily != typedOther._fontFamily) + return false; + final Int8List encodedList = _encoded.buffer.asInt8List(); + final Int8List otherEncodedList = typedOther._encoded.buffer.asInt8List(); + for (int index = 0; index < _encoded.lengthInBytes; index += 1) { + if (encodedList[index] != otherEncodedList[index]) + return false; + } + if (!_listEquals(_fontFamilyFallback, typedOther._fontFamilyFallback)) + return false; + return true; + } + + @override + int get hashCode => hashValues(hashList(_encoded.buffer.asInt8List()), _fontFamily); + +} + /// A direction in which text flows. /// /// Some languages are written from the left to the right (for example, English, @@ -1207,8 +1387,18 @@ class ParagraphBuilder extends NativeFieldWrapperClass2 { /// Creates a [ParagraphBuilder] object, which is used to create a /// [Paragraph]. @pragma('vm:entry-point') - ParagraphBuilder(ParagraphStyle style) { _constructor(style._encoded, style._fontFamily, style._fontSize, style._lineHeight, style._ellipsis, _encodeLocale(style._locale)); } - void _constructor(Int32List encoded, String fontFamily, double fontSize, double lineHeight, String ellipsis, String locale) native 'ParagraphBuilder_constructor'; + ParagraphBuilder(ParagraphStyle style) { + List strutFontFamilies; + if (style._strutStyle != null) { + strutFontFamilies = []; + if (style._strutStyle._fontFamily != null) + strutFontFamilies.add(style._strutStyle._fontFamily); + if (style._strutStyle._fontFamilyFallback != null) + strutFontFamilies.addAll(style._strutStyle._fontFamilyFallback); + } + _constructor(style._encoded, style._strutStyle?._encoded, style._fontFamily, strutFontFamilies, style._fontSize, style._height, style._ellipsis, _encodeLocale(style._locale)); + } + void _constructor(Int32List encoded, ByteData strutData, String fontFamily, List strutFontFamily, double fontSize, double height, String ellipsis, String locale) native 'ParagraphBuilder_constructor'; /// Applies the given style to the added text until [pop] is called. /// diff --git a/lib/ui/text/paragraph_builder.cc b/lib/ui/text/paragraph_builder.cc index 61a1bc2d2dfd2..5f5fb8c814dfc 100644 --- a/lib/ui/text/paragraph_builder.cc +++ b/lib/ui/text/paragraph_builder.cc @@ -72,9 +72,10 @@ const int psFontStyleIndex = 4; const int psMaxLinesIndex = 5; const int psFontFamilyIndex = 6; const int psFontSizeIndex = 7; -const int psLineHeightIndex = 8; -const int psEllipsisIndex = 9; -const int psLocaleIndex = 10; +const int psHeightIndex = 8; +const int psStrutStyleIndex = 9; +const int psEllipsisIndex = 10; +const int psLocaleIndex = 11; const int psTextAlignMask = 1 << psTextAlignIndex; const int psTextDirectionMask = 1 << psTextDirectionIndex; @@ -83,7 +84,8 @@ const int psFontStyleMask = 1 << psFontStyleIndex; const int psMaxLinesMask = 1 << psMaxLinesIndex; const int psFontFamilyMask = 1 << psFontFamilyIndex; const int psFontSizeMask = 1 << psFontSizeIndex; -const int psLineHeightMask = 1 << psLineHeightIndex; +const int psHeightMask = 1 << psHeightIndex; +const int psStrutStyleMask = 1 << psStrutStyleIndex; const int psEllipsisMask = 1 << psEllipsisIndex; const int psLocaleMask = 1 << psLocaleIndex; @@ -97,6 +99,23 @@ constexpr uint32_t kXOffset = 1; constexpr uint32_t kYOffset = 2; constexpr uint32_t kBlurOffset = 3; +// Strut decoding +const int sFontWeightIndex = 0; +const int sFontStyleIndex = 1; +const int sFontFamilyIndex = 2; +const int sFontSizeIndex = 3; +const int sHeightIndex = 4; +const int sLeadingIndex = 5; +const int sForceStrutHeightIndex = 6; + +const int sFontWeightMask = 1 << sFontWeightIndex; +const int sFontStyleMask = 1 << sFontStyleIndex; +const int sFontFamilyMask = 1 << sFontFamilyIndex; +const int sFontSizeMask = 1 << sFontSizeIndex; +const int sHeightMask = 1 << sHeightIndex; +const int sLeadingMask = 1 << sLeadingIndex; +const int sForceStrutHeightMask = 1 << sForceStrutHeightIndex; + } // namespace static void ParagraphBuilder_constructor(Dart_NativeArguments args) { @@ -115,50 +134,124 @@ FOR_EACH_BINDING(DART_NATIVE_CALLBACK) void ParagraphBuilder::RegisterNatives(tonic::DartLibraryNatives* natives) { natives->Register( - {{"ParagraphBuilder_constructor", ParagraphBuilder_constructor, 7, true}, + {{"ParagraphBuilder_constructor", ParagraphBuilder_constructor, 9, true}, FOR_EACH_BINDING(DART_REGISTER_NATIVE)}); } fml::RefPtr ParagraphBuilder::create( tonic::Int32List& encoded, + Dart_Handle strutData, const std::string& fontFamily, + const std::vector& strutFontFamilies, double fontSize, - double lineHeight, + double height, const std::u16string& ellipsis, const std::string& locale) { - return fml::MakeRefCounted(encoded, fontFamily, fontSize, - lineHeight, ellipsis, locale); + return fml::MakeRefCounted(encoded, strutData, fontFamily, + strutFontFamilies, fontSize, + height, ellipsis, locale); +} + +// returns true if there is a font family defined. Font family is the only +// parameter passed directly. +void decodeStrut(Dart_Handle strut_data, + const std::vector& strut_font_families, + txt::ParagraphStyle& paragraph_style) { + if (strut_data == Dart_Null()) { + return; + } + + tonic::DartByteData byte_data(strut_data); + if (byte_data.length_in_bytes() == 0) { + return; + } + paragraph_style.strut_enabled = true; + + const uint8_t* uint8_data = static_cast(byte_data.data()); + uint8_t mask = uint8_data[0]; + + // Data is stored in order of increasing size, eg, 8 bit ints will be before + // any 32 bit ints. In addition, the order of decoding is the same order + // as it is encoded, and the order is used to maintain consistency. + size_t byte_count = 1; + if (mask & sFontWeightMask) { + paragraph_style.strut_font_weight = + static_cast(uint8_data[byte_count++]); + } + if (mask & sFontStyleMask) { + paragraph_style.strut_font_style = + static_cast(uint8_data[byte_count++]); + } + + float float_data[byte_data.length_in_bytes() - byte_count / 4]; + memcpy(float_data, static_cast(byte_data.data()) + byte_count, + byte_data.length_in_bytes() - byte_count); + size_t float_count = 0; + if (mask & sFontSizeMask) { + paragraph_style.strut_font_size = float_data[float_count++]; + } + if (mask & sHeightMask) { + paragraph_style.strut_height = float_data[float_count++]; + } + if (mask & sLeadingMask) { + paragraph_style.strut_leading = float_data[float_count++]; + } + if (mask & sForceStrutHeightMask) { + // The boolean is stored as the last bit in the bitmask. + paragraph_style.force_strut_height = (mask & 1 << 7) != 0; + } + + if (mask & sFontFamilyMask) { + paragraph_style.strut_font_families = strut_font_families; + } else { + // Provide an empty font name so that the platform default font will be + // used. + paragraph_style.strut_font_families.push_back(""); + } } -ParagraphBuilder::ParagraphBuilder(tonic::Int32List& encoded, - const std::string& fontFamily, - double fontSize, - double lineHeight, - const std::u16string& ellipsis, - const std::string& locale) { +ParagraphBuilder::ParagraphBuilder( + tonic::Int32List& encoded, + Dart_Handle strutData, + const std::string& fontFamily, + const std::vector& strutFontFamilies, + double fontSize, + double height, + const std::u16string& ellipsis, + const std::string& locale) { int32_t mask = encoded[0]; txt::ParagraphStyle style; + if (mask & psTextAlignMask) style.text_align = txt::TextAlign(encoded[psTextAlignIndex]); if (mask & psTextDirectionMask) style.text_direction = txt::TextDirection(encoded[psTextDirectionIndex]); - if (mask & psFontWeightMask) + if (mask & psFontWeightMask) { style.font_weight = static_cast(encoded[psFontWeightIndex]); + } - if (mask & psFontStyleMask) + if (mask & psFontStyleMask) { style.font_style = static_cast(encoded[psFontStyleIndex]); + } - if (mask & psFontFamilyMask) + if (mask & psFontFamilyMask) { style.font_family = fontFamily; + } - if (mask & psFontSizeMask) + if (mask & psFontSizeMask) { style.font_size = fontSize; + } - if (mask & psLineHeightMask) - style.line_height = lineHeight; + if (mask & psHeightMask) { + style.height = height; + } + + if (mask & psStrutStyleMask) { + decodeStrut(strutData, strutFontFamilies, style); + } if (mask & psMaxLinesMask) style.max_lines = encoded[psMaxLinesIndex]; diff --git a/lib/ui/text/paragraph_builder.h b/lib/ui/text/paragraph_builder.h index 69183557fa194..6370e45fc708d 100644 --- a/lib/ui/text/paragraph_builder.h +++ b/lib/ui/text/paragraph_builder.h @@ -25,12 +25,15 @@ class ParagraphBuilder : public RefCountedDartWrappable { FML_FRIEND_MAKE_REF_COUNTED(ParagraphBuilder); public: - static fml::RefPtr create(tonic::Int32List& encoded, - const std::string& fontFamily, - double fontSize, - double lineHeight, - const std::u16string& ellipsis, - const std::string& locale); + static fml::RefPtr create( + tonic::Int32List& encoded, + Dart_Handle strutData, + const std::string& fontFamily, + const std::vector& strutFontFamilies, + double fontSize, + double height, + const std::u16string& ellipsis, + const std::string& locale); ~ParagraphBuilder() override; @@ -57,9 +60,11 @@ class ParagraphBuilder : public RefCountedDartWrappable { private: explicit ParagraphBuilder(tonic::Int32List& encoded, + Dart_Handle strutData, const std::string& fontFamily, + const std::vector& strutFontFamilies, double fontSize, - double lineHeight, + double height, const std::u16string& ellipsis, const std::string& locale); diff --git a/third_party/txt/src/txt/font_collection.cc b/third_party/txt/src/txt/font_collection.cc index ff687726ebed9..1592d9227ccfe 100644 --- a/third_party/txt/src/txt/font_collection.cc +++ b/third_party/txt/src/txt/font_collection.cc @@ -178,6 +178,26 @@ FontCollection::GetMinikinFontCollectionForFamilies( return font_collection; } +minikin::MinikinFont* FontCollection::GetMinikinFontForFamilies( + const std::vector& font_families, + minikin::FontStyle style) { + std::shared_ptr font_family = nullptr; + for (std::string family_name : font_families) { + font_family = FindFontFamilyInManagers(family_name); + if (font_family != nullptr) { + break; + } + } + if (font_family == nullptr) { + const auto default_font_family = GetDefaultFontFamily(); + font_family = FindFontFamilyInManagers(default_font_family); + } + if (font_family == nullptr) { + return nullptr; + } + return font_family.get()->getClosestMatch(style).font; +} + std::shared_ptr FontCollection::FindFontFamilyInManagers( const std::string& family_name) { // Search for the font family in each font manager. diff --git a/third_party/txt/src/txt/font_collection.h b/third_party/txt/src/txt/font_collection.h index c98b4d29ca05d..fe05c9f6d97be 100644 --- a/third_party/txt/src/txt/font_collection.h +++ b/third_party/txt/src/txt/font_collection.h @@ -49,6 +49,10 @@ class FontCollection : public std::enable_shared_from_this { const std::vector& font_families, const std::string& locale); + minikin::MinikinFont* GetMinikinFontForFamilies( + const std::vector& font_families, + minikin::FontStyle style); + // Provides a FontFamily that contains glyphs for ch. This caches previously // matched fonts. Also see FontCollection::DoMatchFallbackFont. const std::shared_ptr& MatchFallbackFont( diff --git a/third_party/txt/src/txt/paragraph.cc b/third_party/txt/src/txt/paragraph.cc index cdbae4be2fa4b..c5e10fa0776be 100644 --- a/third_party/txt/src/txt/paragraph.cc +++ b/third_party/txt/src/txt/paragraph.cc @@ -290,7 +290,7 @@ bool Paragraph::ComputeLineBreaks() { std::shared_ptr collection = GetMinikinFontCollectionForStyle(run.style); if (collection == nullptr) { - FML_LOG(INFO) << "Could not find font collection for family \"" + FML_LOG(INFO) << "Could not find font collection for families \"" << (run.style.font_families.empty() ? "" : run.style.font_families[0]) @@ -424,6 +424,52 @@ bool Paragraph::ComputeBidiRuns(std::vector* result) { return true; } +void Paragraph::ComputeStrut(StrutMetrics* strut, SkFont& font) { + strut->ascent = 0; + strut->descent = 0; + strut->leading = 0; + strut->half_leading = 0; + strut->line_height = 0; + strut->force_strut = false; + + // Font size must be positive. + bool valid_strut = + paragraph_style_.strut_enabled && paragraph_style_.strut_font_size >= 0; + if (!valid_strut) { + return; + } + // force_strut makes all lines have exactly the strut metrics, and ignores all + // actual metrics. We only force the strut if the strut is non-zero and valid. + strut->force_strut = paragraph_style_.force_strut_height && valid_strut; + const FontSkia* font_skia = + static_cast(font_collection_->GetMinikinFontForFamilies( + paragraph_style_.strut_font_families, + // TODO(garyq): The variant is currently set to 0 (default) as we do + // not have a property to set it with. We should eventually support + // default, compact, and elegant variants. + minikin::FontStyle( + 0, GetWeight(paragraph_style_.strut_font_weight), + paragraph_style_.strut_font_style == FontStyle::italic))); + + if (font_skia != nullptr) { + font.setTypeface(font_skia->GetSkTypeface()); + font.setSize(paragraph_style_.strut_font_size); + SkFontMetrics strut_metrics; + font.getMetrics(&strut_metrics); + + strut->ascent = paragraph_style_.strut_height * -strut_metrics.fAscent; + strut->descent = paragraph_style_.strut_height * strut_metrics.fDescent; + strut->leading = + // Use font's leading if there is no user specified strut leading. + paragraph_style_.strut_leading < 0 + ? strut_metrics.fLeading + : (paragraph_style_.strut_leading * + (strut_metrics.fDescent - strut_metrics.fAscent)); + strut->half_leading = strut->leading / 2; + strut->line_height = strut->ascent + strut->descent + strut->leading; + } +} + void Paragraph::Layout(double width, bool force) { // Do not allow calling layout multiple times without changing anything. if (!needs_layout_ && width == width_ && !force) { @@ -462,6 +508,11 @@ void Paragraph::Layout(double width, bool force) { double prev_max_descent = 0; double max_word_width = 0; + // Compute strut minimums according to paragraph_style_. + StrutMetrics strut; + ComputeStrut(&strut, font); + + // Paragraph bounds tracking. size_t line_limit = std::min(paragraph_style_.max_lines, line_ranges_.size()); did_exceed_max_lines_ = (line_ranges_.size() > paragraph_style_.max_lines); @@ -757,36 +808,29 @@ void Paragraph::Layout(double width, bool force) { line_code_unit_runs.back().direction); } - double max_line_spacing = 0; - double max_descent = 0; + // Calculate the amount to advance in the y direction. This is done by + // computing the maximum ascent and descent with respect to the strut. + double max_ascent = strut.ascent + strut.half_leading; + double max_descent = strut.descent + strut.half_leading; SkScalar max_unscaled_ascent = 0; auto update_line_metrics = [&](const SkFontMetrics& metrics, const TextStyle& style) { - // TODO(garyq): Multipling in the style.height on the first line is - // probably wrong. Figure out how paragraph and line heights are supposed - // to work and fix it. - double line_spacing = - (line_number == 0) - ? -metrics.fAscent * style.height - : (-metrics.fAscent + metrics.fLeading) * style.height; - if (line_spacing > max_line_spacing) { - max_line_spacing = line_spacing; - if (line_number == 0) { - alphabetic_baseline_ = line_spacing; - ideographic_baseline_ = - (metrics.fDescent - metrics.fAscent) * style.height; - } + if (!strut.force_strut) { + double ascent = + (-metrics.fAscent + metrics.fLeading / 2) * style.height; + max_ascent = std::max(ascent, max_ascent); + + double descent = + (metrics.fDescent + metrics.fLeading / 2) * style.height; + max_descent = std::max(descent, max_descent); } - max_line_spacing = std::max(line_spacing, max_line_spacing); - - double descent = metrics.fDescent * style.height; - max_descent = std::max(descent, max_descent); max_unscaled_ascent = std::max(-metrics.fAscent, max_unscaled_ascent); }; for (const PaintRecord& paint_record : paint_records) { update_line_metrics(paint_record.metrics(), paint_record.style()); } + // If no fonts were actually rendered, then compute a baseline based on the // font of the paragraph style. if (paint_records.empty()) { @@ -798,17 +842,24 @@ void Paragraph::Layout(double width, bool force) { update_line_metrics(metrics, style); } - // TODO(garyq): Remove rounding of line heights because it is irrelevant in - // a world of high DPI devices. + // Calculate the baselines. This is only done on the first line. + if (line_number == 0) { + alphabetic_baseline_ = max_ascent; + // TODO(garyq): Ideographic baseline is currently bottom of EM + // box, which is not correct. This should be obtained from metrics. + // Skia currently does not support various baselines. + ideographic_baseline_ = (max_ascent + max_descent); + } + line_heights_.push_back((line_heights_.empty() ? 0 : line_heights_.back()) + - round(max_line_spacing + max_descent)); + round(max_ascent + max_descent)); line_baselines_.push_back(line_heights_.back() - max_descent); - y_offset += round(max_line_spacing + prev_max_descent); + y_offset += round(max_ascent + prev_max_descent); prev_max_descent = max_descent; // The max line spacing and ascent have been multiplied by -1 to make math // in GetRectsForRange more logical/readable. - line_max_spacings_.push_back(max_line_spacing); + line_max_spacings_.push_back(max_ascent); line_max_descent_.push_back(max_descent); line_max_ascent_.push_back(max_unscaled_ascent); diff --git a/third_party/txt/src/txt/paragraph.h b/third_party/txt/src/txt/paragraph.h index 7cee95379d271..3ce1ae197973f 100644 --- a/third_party/txt/src/txt/paragraph.h +++ b/third_party/txt/src/txt/paragraph.h @@ -352,6 +352,16 @@ class Paragraph { : x_start(x_s), y_start(y_s), x_end(x_e), y_end(y_e) {} }; + // Strut metrics of zero will have no effect on the layout. + struct StrutMetrics { + double ascent = 0; // Positive value to keep signs clear. + double descent = 0; + double leading = 0; + double half_leading = 0; + double line_height = 0; + bool force_strut = false; + }; + // Passes in the text and Styled Runs. text_ and runs_ will later be passed // into breaker_ in InitBreaker(), which is called in Layout(). void SetText(std::vector text, StyledRuns runs); @@ -366,6 +376,9 @@ class Paragraph { // Break the text into runs based on LTR/RTL text direction. bool ComputeBidiRuns(std::vector* result); + // Calculates and populates strut based on paragraph_style_ strut info. + void ComputeStrut(StrutMetrics* strut, SkFont& font); + // Calculate the starting X offset of a line based on the line's width and // alignment. double GetLineXOffset(double line_total_advance); diff --git a/third_party/txt/src/txt/paragraph_style.cc b/third_party/txt/src/txt/paragraph_style.cc index f4849f4df667f..933a8db6d6123 100644 --- a/third_party/txt/src/txt/paragraph_style.cc +++ b/third_party/txt/src/txt/paragraph_style.cc @@ -25,9 +25,11 @@ TextStyle ParagraphStyle::GetTextStyle() const { result.font_weight = font_weight; result.font_style = font_style; result.font_families = std::vector({font_family}); - result.font_size = font_size; + if (font_size >= 0) { + result.font_size = font_size; + } result.locale = locale; - result.height = line_height; + result.height = height; return result; } diff --git a/third_party/txt/src/txt/paragraph_style.h b/third_party/txt/src/txt/paragraph_style.h index 3b04269d8eaee..a286b4a02327f 100644 --- a/third_party/txt/src/txt/paragraph_style.h +++ b/third_party/txt/src/txt/paragraph_style.h @@ -43,15 +43,31 @@ enum class TextDirection { class ParagraphStyle { public: + // Default TextStyle. Used in GetTextStyle() to obtain the base TextStyle to + // inherit off of. FontWeight font_weight = FontWeight::w400; FontStyle font_style = FontStyle::normal; std::string font_family = ""; double font_size = 14; + double height = 1; + // Strut properties. strut_enabled must be set to true for the rest of the + // properties to take effect. + // TODO(garyq): Break the strut properties into a separate class. + bool strut_enabled = false; + FontWeight strut_font_weight = FontWeight::w400; + FontStyle strut_font_style = FontStyle::normal; + std::vector strut_font_families; + double strut_font_size = 14; + double strut_height = 1; + double strut_leading = -1; // Negative to use font's default leading. [0,inf) + // to use custom leading as a ratio of font size. + bool force_strut_height = false; + + // General paragraph properties. TextAlign text_align = TextAlign::start; TextDirection text_direction = TextDirection::ltr; size_t max_lines = std::numeric_limits::max(); - double line_height = 1.0; std::u16string ellipsis; std::string locale; diff --git a/third_party/txt/src/txt/text_style.h b/third_party/txt/src/txt/text_style.h index f3c5bde46a761..925374ab33955 100644 --- a/third_party/txt/src/txt/text_style.h +++ b/third_party/txt/src/txt/text_style.h @@ -55,6 +55,8 @@ class TextStyle { SkPaint background; bool has_foreground = false; SkPaint foreground; + // An ordered list of shadows where the first shadow will be drawn first (at + // the bottom). std::vector text_shadows; TextStyle(); diff --git a/third_party/txt/tests/paragraph_unittests.cc b/third_party/txt/tests/paragraph_unittests.cc index 963a5345e7367..13c41e4890401 100644 --- a/third_party/txt/tests/paragraph_unittests.cc +++ b/third_party/txt/tests/paragraph_unittests.cc @@ -2571,7 +2571,7 @@ TEST_F(ParagraphTest, BaselineParagraph) { txt::ParagraphStyle paragraph_style; paragraph_style.max_lines = 14; paragraph_style.text_align = TextAlign::justify; - paragraph_style.line_height = 1.5; + paragraph_style.height = 1.5; txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); txt::TextStyle text_style; @@ -2606,7 +2606,7 @@ TEST_F(ParagraphTest, BaselineParagraph) { GetCanvas()->drawLine(0, paragraph->GetAlphabeticBaseline(), paragraph->GetMaxWidth(), paragraph->GetAlphabeticBaseline(), paint); - ASSERT_DOUBLE_EQ(paragraph->GetIdeographicBaseline(), 79.035003662109375); + ASSERT_DOUBLE_EQ(paragraph->GetIdeographicBaseline(), 79.035000801086426); ASSERT_DOUBLE_EQ(paragraph->GetAlphabeticBaseline(), 63.305000305175781); ASSERT_TRUE(Snapshot()); @@ -2703,4 +2703,526 @@ TEST_F(ParagraphTest, FontFallbackParagraph) { 0); } +// Disabled due to Skia depending on platform to get metrics, which +// results in presubmit runs to have different values. +// +// TODO(garyq): Re-enable strut tests, allow font metric fakery, or +// consolidate skia font metric behavior. +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(DISABLED_StrutParagraph1)) { + // The chinese extra height should be absorbed by the strut. + const char* text = "01234満毎冠p来É本可\nabcd\n満毎É行p昼本可"; + 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.max_lines = 10; + paragraph_style.strut_font_families = std::vector(1, "BlahFake"); + paragraph_style.strut_font_families.push_back("ahem"); + paragraph_style.strut_font_size = 50; + paragraph_style.strut_height = 1.5; + paragraph_style.strut_leading = 0.1; + paragraph_style.strut_enabled = true; + + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "ahem"); + text_style.font_families.push_back("ahem"); + text_style.font_size = 50; + text_style.letter_spacing = 0; + text_style.font_weight = FontWeight::w500; + text_style.word_spacing = 0; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(550); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + // Tests for GetRectsForRange() + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kTight; + Paragraph::RectHeightStyle rect_height_max_style = + Paragraph::RectHeightStyle::kMax; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForRange(0, 0, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 0ull); + + boxes = + paragraph->GetRectsForRange(0, 1, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 30.313477); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 50); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 80.313477); + + boxes = paragraph->GetRectsForRange(0, 1, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 30.313477); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 50); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 89); + + boxes = + paragraph->GetRectsForRange(6, 10, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 300); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 30.313477); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 500); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 80.313477); + + boxes = paragraph->GetRectsForRange(6, 10, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 300); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 30.313477); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 500); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 89); + + boxes = paragraph->GetRectsForRange(14, 16, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 208.31348); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 100); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 267); + + boxes = paragraph->GetRectsForRange(20, 25, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 50); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 297.31348); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 300); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 356); + + ASSERT_TRUE(Snapshot()); +} + +// Disabled due to Skia depending on platform to get metrics, which +// results in presubmit runs to have different values. +// +// TODO(garyq): Re-enable strut tests, allow font metric fakery, or +// consolidate skia font metric behavior. +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(DISABLED_StrutParagraph2)) { + // This string is all one size and smaller than the strut metrics. + const char* text = "01234ABCDEFGH\nabcd\nABCDEFGH"; + 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.max_lines = 10; + paragraph_style.strut_font_families = std::vector(1, "ahem"); + paragraph_style.strut_font_size = 50; + paragraph_style.strut_height = 1.6; + paragraph_style.strut_enabled = true; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "ahem"); + text_style.font_families.push_back("ahem"); + text_style.font_size = 50; + text_style.letter_spacing = 0; + text_style.font_weight = FontWeight::w500; + text_style.word_spacing = 0; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(550); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + // Tests for GetRectsForRange() + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kTight; + Paragraph::RectHeightStyle rect_height_max_style = + Paragraph::RectHeightStyle::kMax; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForRange(0, 0, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 0ull); + + boxes = + paragraph->GetRectsForRange(0, 1, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 33.229004); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 50); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 83.229004); + + boxes = paragraph->GetRectsForRange(0, 1, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 33.229004); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 50); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 91); + + boxes = + paragraph->GetRectsForRange(6, 10, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 300); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 33.229004); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 500); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 83.229004); + + boxes = paragraph->GetRectsForRange(6, 10, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 300); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 33.229004); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 500); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 91); + + boxes = paragraph->GetRectsForRange(14, 16, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 215.229); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 100); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 273); + + boxes = paragraph->GetRectsForRange(20, 25, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 50); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 306.229); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 300); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 364); + + ASSERT_TRUE(Snapshot()); +} + +// Disabled due to Skia depending on platform to get metrics, which +// results in presubmit runs to have different values. +// +// TODO(garyq): Re-enable strut tests, allow font metric fakery, or +// consolidate skia font metric behavior. +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(DISABLED_StrutParagraph3)) { + // The strut is too small to absorb the extra chinese height, but the english + // second line height is increased due to strut. + const char* text = "01234満毎p行来昼本可\nabcd\n満毎冠行来昼本可"; + 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.max_lines = 10; + paragraph_style.strut_font_families = std::vector(1, "ahem"); + paragraph_style.strut_font_size = 50; + paragraph_style.strut_height = 1.1; + paragraph_style.strut_enabled = true; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "ahem"); + text_style.font_families.push_back("ahem"); + text_style.font_size = 50; + text_style.letter_spacing = 0; + text_style.font_weight = FontWeight::w500; + text_style.word_spacing = 0; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(550); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + // Tests for GetRectsForRange() + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kTight; + Paragraph::RectHeightStyle rect_height_max_style = + Paragraph::RectHeightStyle::kMax; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForRange(0, 0, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 0ull); + + boxes = + paragraph->GetRectsForRange(0, 1, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 10.526855); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 50); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 60.526855); + + boxes = paragraph->GetRectsForRange(0, 1, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 10.526855); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 50); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 63); + + boxes = + paragraph->GetRectsForRange(6, 10, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 300); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 10.526855); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 500); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 60.526855); + + boxes = paragraph->GetRectsForRange(6, 10, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 300); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 10.526855); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 500); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 63); + + boxes = paragraph->GetRectsForRange(14, 16, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 136.52686); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 100); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 189); + + boxes = paragraph->GetRectsForRange(20, 25, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 50); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 199.52686); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 300); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 252); + + ASSERT_TRUE(Snapshot()); +} + +// Disabled due to Skia depending on platform to get metrics, which +// results in presubmit runs to have different values. +// +// TODO(garyq): Re-enable strut tests, allow font metric fakery, or +// consolidate skia font metric behavior. +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(DISABLED_StrutForceParagraph)) { + // The strut is too small to absorb the extra chinese height, but the english + // second line height is increased due to strut. + const char* text = "01234満毎冠行来昼本可\nabcd\n満毎冠行来昼本可"; + 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.max_lines = 10; + paragraph_style.strut_font_families = std::vector(1, "ahem"); + paragraph_style.strut_font_size = 50; + paragraph_style.strut_height = 1.5; + paragraph_style.strut_leading = 0.1; + paragraph_style.force_strut_height = true; + paragraph_style.strut_enabled = true; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "ahem"); + text_style.font_families.push_back("ahem"); + text_style.font_size = 50; + text_style.letter_spacing = 0; + text_style.word_spacing = 0; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(550); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + // Tests for GetRectsForRange() + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kTight; + Paragraph::RectHeightStyle rect_height_max_style = + Paragraph::RectHeightStyle::kMax; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForRange(0, 0, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 0ull); + + boxes = + paragraph->GetRectsForRange(0, 1, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 30.313477); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 50); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 80.313477); + + boxes = paragraph->GetRectsForRange(0, 1, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 30.313477); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 50); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 89); + + boxes = + paragraph->GetRectsForRange(6, 10, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 300); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 30.313477); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 500); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 80.313477); + + boxes = paragraph->GetRectsForRange(6, 10, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 300); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 30.313477); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 500); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 89); + + boxes = paragraph->GetRectsForRange(14, 16, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 208.31348); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 100); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 267); + + boxes = paragraph->GetRectsForRange(20, 25, rect_height_max_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 50); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 297.31348); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 300); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 356); + + ASSERT_TRUE(Snapshot()); +} + } // namespace txt