diff --git a/lib/ui/text.dart b/lib/ui/text.dart index f4c78a8abb18f..54f794938e5be 100644 --- a/lib/ui/text.dart +++ b/lib/ui/text.dart @@ -1565,14 +1565,46 @@ enum TextDecorationStyle { wavy } +/// {@macro dart.ui.textLeadingDistribution} +enum TextLeadingDistribution { + /// Distributes the [leading](https://en.wikipedia.org/wiki/Leading) + /// of the text proportionally above and below the text, to the font's + /// ascent/discent ratio. + /// + /// {@template dart.ui.leading} + /// The leading of a text run is defined as + /// `TextStyle.height * TextStyle.fontSize - TextStyle.fontSize`. When + /// [TextStyle.height] is not set, the text run uses the leading specified by + /// the font instead. + /// {@endtemplate} + proportional, + + /// Distributes the ["leading"](https://en.wikipedia.org/wiki/Leading) + /// of the text evenly above and below the text (i.e. evenly above the + /// font's ascender and below the descender). + /// + /// {@macro dart.ui.leading} + /// + /// The leading can become negative when [TextStyle.height] is smaller than + /// 1.0. + /// + /// This is the default strategy used by CSS, known as + /// ["half-leading"](https://www.w3.org/TR/css-inline-3/#half-leading). + even, +} + /// {@template dart.ui.textHeightBehavior} -/// Defines how the paragraph will apply [TextStyle.height] to the ascent of the -/// first line and descent of the last line. +/// Defines how to apply [TextStyle.height] over and under text. +/// +/// [applyHeightToFirstAscent] and [applyHeightToLastDescent] represent whether +/// the [TextStyle.height] modifier will be applied to the corresponding metric. +/// By default both properties are true, and [TextStyle.height] is applied as +/// normal. When set to false, the font's default ascent will be used. +/// +/// [leadingDistribution] determines how the [leading] is distributed over and +/// under text. This property applies before [applyHeightToFirstAscent] and +/// [applyHeightToLastDescent]. /// -/// Each boolean value represents whether the [TextStyle.height] modifier will -/// be applied to the corresponding metric. By default, all properties are true, -/// and [TextStyle.height] is applied as normal. When set to false, the font's -/// default ascent will be used. /// {@endtemplate} class TextHeightBehavior { @@ -1584,20 +1616,23 @@ class TextHeightBehavior { /// * applyHeightToLastDescent: When true, the [TextStyle.height] modifier /// will be applied to the descent of the last line. When false, the font's /// default descent will be used. + /// * leadingDistribution: How the [leading] is distributed over and under + /// text. /// /// All properties default to true (height modifications applied as normal). const TextHeightBehavior({ this.applyHeightToFirstAscent = true, this.applyHeightToLastDescent = true, + this.leadingDistribution = TextLeadingDistribution.proportional, }); /// Creates a new TextHeightBehavior object from an encoded form. /// /// See [encode] for the creation of the encoded form. - const TextHeightBehavior.fromEncoded(int encoded) + TextHeightBehavior.fromEncoded(int encoded) : applyHeightToFirstAscent = (encoded & 0x1) == 0, - applyHeightToLastDescent = (encoded & 0x2) == 0; - + applyHeightToLastDescent = (encoded & 0x2) == 0, + leadingDistribution = TextLeadingDistribution.values[encoded >> 2]; /// Whether to apply the [TextStyle.height] modifier to the ascent of the first /// line in the paragraph. @@ -1623,9 +1658,23 @@ class TextHeightBehavior { /// Defaults to true (height modifications applied as normal). final bool applyHeightToLastDescent; + /// {@template dart.ui.textLeadingDistribution} + /// How the ["leading"](https://en.wikipedia.org/wiki/Leading) is distributed + /// over and under the text. + /// + /// Does not affect layout when [TextStyle.height] is not specified. The + /// leading can become negative, for example, when [TextLeadingDistribution.even] + /// is used with a [TextStyle.height] much smaller than 1.0. + /// {@endtemplate} + /// + /// Defaults to [TextLeadingDistribution.proportional], + final TextLeadingDistribution leadingDistribution; + /// Returns an encoded int representation of this object. int encode() { - return (applyHeightToFirstAscent ? 0 : 1 << 0) | (applyHeightToLastDescent ? 0 : 1 << 1); + return (applyHeightToFirstAscent ? 0 : 1 << 0) + | (applyHeightToLastDescent ? 0 : 1 << 1) + | (leadingDistribution.index << 2); } @override @@ -1634,7 +1683,8 @@ class TextHeightBehavior { return false; return other is TextHeightBehavior && other.applyHeightToFirstAscent == applyHeightToFirstAscent - && other.applyHeightToLastDescent == applyHeightToLastDescent; + && other.applyHeightToLastDescent == applyHeightToLastDescent + && other.leadingDistribution == leadingDistribution; } @override @@ -1642,6 +1692,7 @@ class TextHeightBehavior { return hashValues( applyHeightToFirstAscent, applyHeightToLastDescent, + leadingDistribution.index, ); } @@ -1649,7 +1700,8 @@ class TextHeightBehavior { String toString() { return 'TextHeightBehavior(' 'applyHeightToFirstAscent: $applyHeightToFirstAscent, ' - 'applyHeightToLastDescent: $applyHeightToLastDescent' + 'applyHeightToLastDescent: $applyHeightToLastDescent, ' + 'leadingDistribution: $leadingDistribution' ')'; } } @@ -1696,6 +1748,8 @@ bool _listEquals(List? a, List? b) { // // - Element 7: The enum index of the |textBaseline|. // +// - Element 8: The encoded value of the |leadingDistribution|. +// Int32List _encodeTextStyle( Color? color, TextDecoration? decoration, @@ -1711,13 +1765,14 @@ Int32List _encodeTextStyle( double? letterSpacing, double? wordSpacing, double? height, + TextLeadingDistribution? leadingDistribution, Locale? locale, Paint? background, Paint? foreground, List? shadows, List? fontFeatures, ) { - final Int32List result = Int32List(8); + final Int32List result = Int32List(9); if (color != null) { result[0] |= 1 << 1; result[1] = color.value; @@ -1746,49 +1801,54 @@ Int32List _encodeTextStyle( result[0] |= 1 << 7; result[7] = textBaseline.index; } - if (decorationThickness != null) { + if (leadingDistribution != null) { result[0] |= 1 << 8; + result[8] = leadingDistribution.index; } - if (fontFamily != null || (fontFamilyFallback != null && fontFamilyFallback.isNotEmpty)) { + if (decorationThickness != null) { result[0] |= 1 << 9; + } + if (fontFamily != null || (fontFamilyFallback != null && fontFamilyFallback.isNotEmpty)) { + result[0] |= 1 << 10; // Passed separately to native. } if (fontSize != null) { - result[0] |= 1 << 10; + result[0] |= 1 << 11; // Passed separately to native. } if (letterSpacing != null) { - result[0] |= 1 << 11; + result[0] |= 1 << 12; // Passed separately to native. } if (wordSpacing != null) { - result[0] |= 1 << 12; + result[0] |= 1 << 13; // Passed separately to native. } if (height != null) { - result[0] |= 1 << 13; + result[0] |= 1 << 14; // Passed separately to native. } if (locale != null) { - result[0] |= 1 << 14; + result[0] |= 1 << 15; // Passed separately to native. } if (background != null) { - result[0] |= 1 << 15; + result[0] |= 1 << 16; // Passed separately to native. } if (foreground != null) { - result[0] |= 1 << 16; + result[0] |= 1 << 17; // Passed separately to native. } if (shadows != null) { - result[0] |= 1 << 17; + result[0] |= 1 << 18; // Passed separately to native. } if (fontFeatures != null) { - result[0] |= 1 << 18; + result[0] |= 1 << 19; // Passed separately to native. } + return result; } @@ -1823,6 +1883,7 @@ class TextStyle { /// * `textBaseline`: The common baseline that should be aligned between this text span and its parent text span, or, for the root text spans, with the line box. /// * `height`: The height of this text span, as a multiplier of the font size. Omitting `height` will allow the line height /// to take the height as defined by the font, which may not be exactly the height of the fontSize. + /// * `leadingDistribution`: When `height` is specified, how the extra vertical space should be distributed over and under the text. /// * `locale`: The locale used to select region-specific glyphs. /// * `background`: The paint drawn as a background for the text. /// * `foreground`: The paint used to draw the text. If this is specified, `color` must be null. @@ -1842,6 +1903,7 @@ class TextStyle { double? letterSpacing, double? wordSpacing, double? height, + TextLeadingDistribution? leadingDistribution, Locale? locale, Paint? background, Paint? foreground, @@ -1866,6 +1928,7 @@ class TextStyle { letterSpacing, wordSpacing, height, + leadingDistribution, locale, background, foreground, @@ -1925,29 +1988,30 @@ class TextStyle { @override String toString() { return 'TextStyle(' - 'color: ${ _encoded[0] & 0x00002 == 0x00002 ? Color(_encoded[1]) : "unspecified"}, ' - 'decoration: ${ _encoded[0] & 0x00004 == 0x00004 ? TextDecoration._(_encoded[2]) : "unspecified"}, ' - 'decorationColor: ${ _encoded[0] & 0x00008 == 0x00008 ? Color(_encoded[3]) : "unspecified"}, ' - 'decorationStyle: ${ _encoded[0] & 0x00010 == 0x00010 ? TextDecorationStyle.values[_encoded[4]] : "unspecified"}, ' + 'color: ${ _encoded[0] & 0x00002 == 0x00002 ? Color(_encoded[1]) : "unspecified"}, ' + 'decoration: ${ _encoded[0] & 0x00004 == 0x00004 ? TextDecoration._(_encoded[2]) : "unspecified"}, ' + 'decorationColor: ${ _encoded[0] & 0x00008 == 0x00008 ? Color(_encoded[3]) : "unspecified"}, ' + 'decorationStyle: ${ _encoded[0] & 0x00010 == 0x00010 ? TextDecorationStyle.values[_encoded[4]] : "unspecified"}, ' // The decorationThickness is not in encoded order in order to keep it near the other decoration properties. - 'decorationThickness: ${_encoded[0] & 0x00100 == 0x00100 ? _decorationThickness : "unspecified"}, ' - 'fontWeight: ${ _encoded[0] & 0x00020 == 0x00020 ? FontWeight.values[_encoded[5]] : "unspecified"}, ' - 'fontStyle: ${ _encoded[0] & 0x00040 == 0x00040 ? FontStyle.values[_encoded[6]] : "unspecified"}, ' - 'textBaseline: ${ _encoded[0] & 0x00080 == 0x00080 ? TextBaseline.values[_encoded[7]] : "unspecified"}, ' - 'fontFamily: ${ _encoded[0] & 0x00200 == 0x00200 - && _fontFamily != '' ? _fontFamily : "unspecified"}, ' - 'fontFamilyFallback: ${ _encoded[0] & 0x00200 == 0x00200 + 'decorationThickness: ${_encoded[0] & 0x00200 == 0x00200 ? _decorationThickness : "unspecified"}, ' + 'fontWeight: ${ _encoded[0] & 0x00020 == 0x00020 ? FontWeight.values[_encoded[5]] : "unspecified"}, ' + 'fontStyle: ${ _encoded[0] & 0x00040 == 0x00040 ? FontStyle.values[_encoded[6]] : "unspecified"}, ' + 'textBaseline: ${ _encoded[0] & 0x00080 == 0x00080 ? TextBaseline.values[_encoded[7]] : "unspecified"}, ' + 'fontFamily: ${ _encoded[0] & 0x00400 == 0x00400 + && _fontFamily != '' ? _fontFamily : "unspecified"}, ' + 'fontFamilyFallback: ${ _encoded[0] & 0x00400 == 0x00400 && _fontFamilyFallback != null - && _fontFamilyFallback!.isNotEmpty ? _fontFamilyFallback : "unspecified"}, ' - 'fontSize: ${ _encoded[0] & 0x00400 == 0x00400 ? _fontSize : "unspecified"}, ' - 'letterSpacing: ${ _encoded[0] & 0x00800 == 0x00800 ? "${_letterSpacing}x" : "unspecified"}, ' - 'wordSpacing: ${ _encoded[0] & 0x01000 == 0x01000 ? "${_wordSpacing}x" : "unspecified"}, ' - 'height: ${ _encoded[0] & 0x02000 == 0x02000 ? "${_height}x" : "unspecified"}, ' - 'locale: ${ _encoded[0] & 0x04000 == 0x04000 ? _locale : "unspecified"}, ' - 'background: ${ _encoded[0] & 0x08000 == 0x08000 ? _background : "unspecified"}, ' - 'foreground: ${ _encoded[0] & 0x10000 == 0x10000 ? _foreground : "unspecified"}, ' - 'shadows: ${ _encoded[0] & 0x20000 == 0x20000 ? _shadows : "unspecified"}, ' - 'fontFeatures: ${ _encoded[0] & 0x40000 == 0x40000 ? _fontFeatures : "unspecified"}' + && _fontFamilyFallback!.isNotEmpty ? _fontFamilyFallback : "unspecified"}, ' + 'fontSize: ${ _encoded[0] & 0x00800 == 0x00800 ? _fontSize : "unspecified"}, ' + 'letterSpacing: ${ _encoded[0] & 0x01000 == 0x01000 ? "${_letterSpacing}x" : "unspecified"}, ' + 'wordSpacing: ${ _encoded[0] & 0x02000 == 0x02000 ? "${_wordSpacing}x" : "unspecified"}, ' + 'height: ${ _encoded[0] & 0x04000 == 0x04000 ? "${_height}x" : "unspecified"}, ' + 'leadingDistribution: ${_encoded[0] & 0x0100 == 0x0100 ? "${TextLeadingDistribution.values[_encoded[8]]}" : "unspecified"}, ' + 'locale: ${ _encoded[0] & 0x08000 == 0x08000 ? _locale : "unspecified"}, ' + 'background: ${ _encoded[0] & 0x10000 == 0x10000 ? _background : "unspecified"}, ' + 'foreground: ${ _encoded[0] & 0x20000 == 0x20000 ? _foreground : "unspecified"}, ' + 'shadows: ${ _encoded[0] & 0x40000 == 0x40000 ? _shadows : "unspecified"}, ' + 'fontFeatures: ${ _encoded[0] & 0x80000 == 0x80000 ? _fontFeatures : "unspecified"}' ')'; } } @@ -2077,6 +2141,9 @@ class ParagraphStyle { /// * `textHeightBehavior`: Specifies how the `height` multiplier is /// applied to ascent of the first line and the descent of the last line. /// + /// * `leadingDistribution`: Specifies how the extra vertical space added by + /// the `height` multiplier should be distributed over and under the text. + /// /// * `fontWeight`: The typeface thickness to use when painting the text /// (e.g., bold). /// @@ -2192,6 +2259,7 @@ ByteData _encodeStrut( List? fontFamilyFallback, double? fontSize, double? height, + TextLeadingDistribution? leadingDistribution, double? leading, FontWeight? fontWeight, FontStyle? fontStyle, @@ -2199,13 +2267,14 @@ ByteData _encodeStrut( if (fontFamily == null && fontSize == null && height == null && + leadingDistribution == null && leading == null && fontWeight == null && fontStyle == null && forceStrutHeight == null) return ByteData(0); - final ByteData data = ByteData(15); // Max size is 15 bytes + final ByteData data = ByteData(16); // Max size is 16 bytes int bitmask = 0; // 8 bit mask int byteCount = 1; if (fontWeight != null) { @@ -2231,17 +2300,22 @@ ByteData _encodeStrut( bitmask |= 1 << 4; data.setFloat32(byteCount, height, _kFakeHostEndian); byteCount += 4; + if (leadingDistribution != null) { + bitmask |= 1 << 5; + data.setInt8(byteCount, leadingDistribution.index); + byteCount += 4; + } } if (leading != null) { - bitmask |= 1 << 5; + bitmask |= 1 << 6; data.setFloat32(byteCount, leading, _kFakeHostEndian); byteCount += 4; } if (forceStrutHeight != null) { - bitmask |= 1 << 6; + bitmask |= 1 << 7; // We store this boolean directly in the bitmask since there is // extra space in the 16 bit int. - bitmask |= (forceStrutHeight ? 1 : 0) << 7; + bitmask |= (forceStrutHeight ? 1 : 0) << 8; } data.setInt8(0, bitmask); @@ -2276,7 +2350,14 @@ class StrutStyle { /// 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. + /// the font size. `fontSize` must be provided for this property to take + /// effect. The leading added by this property is distributed evenly over + /// and under the text, regardless of `leadingDistribution`. + /// + /// * `leadingDistribution`: how the extra vertical space added by the + /// `height` multiplier should be distributed over and under the text, + /// independent of `leading` (which is always distributed evenly over and + /// under text). /// /// * `fontWeight`: The typeface thickness to use when painting the text /// (e.g., bold). @@ -2296,6 +2377,7 @@ class StrutStyle { List? fontFamilyFallback, double? fontSize, double? height, + TextLeadingDistribution? leadingDistribution, double? leading, FontWeight? fontWeight, FontStyle? fontStyle, @@ -2305,6 +2387,7 @@ class StrutStyle { fontFamilyFallback, fontSize, height, + leadingDistribution, leading, fontWeight, fontStyle, diff --git a/lib/ui/text/paragraph_builder.cc b/lib/ui/text/paragraph_builder.cc index 79dbe23b9957a..d53ad7821044f 100644 --- a/lib/ui/text/paragraph_builder.cc +++ b/lib/ui/text/paragraph_builder.cc @@ -39,17 +39,18 @@ const int tsTextDecorationStyleIndex = 4; const int tsFontWeightIndex = 5; const int tsFontStyleIndex = 6; const int tsTextBaselineIndex = 7; -const int tsTextDecorationThicknessIndex = 8; -const int tsFontFamilyIndex = 9; -const int tsFontSizeIndex = 10; -const int tsLetterSpacingIndex = 11; -const int tsWordSpacingIndex = 12; -const int tsHeightIndex = 13; -const int tsLocaleIndex = 14; -const int tsBackgroundIndex = 15; -const int tsForegroundIndex = 16; -const int tsTextShadowsIndex = 17; -const int tsFontFeaturesIndex = 18; +const int tsLeadingDistributionIndex = 8; +const int tsTextDecorationThicknessIndex = 9; +const int tsFontFamilyIndex = 10; +const int tsFontSizeIndex = 11; +const int tsLetterSpacingIndex = 12; +const int tsWordSpacingIndex = 13; +const int tsHeightIndex = 14; +const int tsLocaleIndex = 15; +const int tsBackgroundIndex = 16; +const int tsForegroundIndex = 17; +const int tsTextShadowsIndex = 18; +const int tsFontFeaturesIndex = 19; const int tsColorMask = 1 << tsColorIndex; const int tsTextDecorationMask = 1 << tsTextDecorationIndex; @@ -59,6 +60,7 @@ const int tsTextDecorationThicknessMask = 1 << tsTextDecorationThicknessIndex; const int tsFontWeightMask = 1 << tsFontWeightIndex; const int tsFontStyleMask = 1 << tsFontStyleIndex; const int tsTextBaselineMask = 1 << tsTextBaselineIndex; +const int tsLeadingDistributionMask = 1 << tsLeadingDistributionIndex; const int tsFontFamilyMask = 1 << tsFontFamilyIndex; const int tsFontSizeMask = 1 << tsFontSizeIndex; const int tsLetterSpacingMask = 1 << tsLetterSpacingIndex; @@ -118,14 +120,16 @@ 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 sLeadingDistributionIndex = 5; +const int sLeadingIndex = 6; +const int sForceStrutHeightIndex = 7; 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 sLeadingDistributionMask = 1 << sLeadingDistributionIndex; const int sLeadingMask = 1 << sLeadingIndex; const int sForceStrutHeightMask = 1 << sForceStrutHeightIndex; @@ -210,6 +214,12 @@ void decodeStrut(Dart_Handle strut_data, if (mask & sHeightMask) { paragraph_style.strut_height = float_data[float_count++]; paragraph_style.strut_has_height_override = true; + + // LeadingDistribution does not affect layout if height is not set. + if (mask & sLeadingDistributionMask) { + paragraph_style.strut_half_leading = uint8_data[byte_count]; + paragraph_style.strut_has_leading_distribution_override = true; + } } if (mask & sLeadingMask) { paragraph_style.strut_leading = float_data[float_count++]; @@ -373,7 +383,7 @@ void ParagraphBuilder::pushStyle(tonic::Int32List& encoded, Dart_Handle foreground_data, Dart_Handle shadows_data, Dart_Handle font_features_data) { - FML_DCHECK(encoded.num_elements() == 8); + FML_DCHECK(encoded.num_elements() == 9); int32_t mask = encoded[0]; @@ -410,6 +420,11 @@ void ParagraphBuilder::pushStyle(tonic::Int32List& encoded, // property wasn't wired up either. } + style.has_leading_distribution_override = mask & tsLeadingDistributionMask; + if (mask & tsLeadingDistributionMask) { + style.half_leading = encoded[tsLeadingDistributionIndex]; + } + if (mask & (tsFontWeightMask | tsFontStyleMask | tsFontSizeMask | tsLetterSpacingMask | tsWordSpacingMask)) { if (mask & tsFontWeightMask) { diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index 3c4207b3719f9..463e763b8eefe 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -1211,6 +1211,8 @@ class EngineStrutStyle implements ui.StrutStyle { List? fontFamilyFallback, double? fontSize, double? height, + //TODO(LongCatIsLooong): implement leadingDistribution. + ui.TextLeadingDistribution? leadingDistribution, double? leading, ui.FontWeight? fontWeight, ui.FontStyle? fontStyle, @@ -1219,6 +1221,7 @@ class EngineStrutStyle implements ui.StrutStyle { _fontFamilyFallback = fontFamilyFallback, _fontSize = fontSize, _height = height, + _leadingDistribution = leadingDistribution, _leading = leading, _fontWeight = fontWeight, _fontStyle = fontStyle, @@ -1232,6 +1235,7 @@ class EngineStrutStyle implements ui.StrutStyle { final ui.FontWeight? _fontWeight; final ui.FontStyle? _fontStyle; final bool? _forceStrutHeight; + final ui.TextLeadingDistribution? _leadingDistribution; @override bool operator ==(Object other) { @@ -1246,6 +1250,7 @@ class EngineStrutStyle implements ui.StrutStyle { && other._fontSize == _fontSize && other._height == _height && other._leading == _leading + && other._leadingDistribution == _leadingDistribution && other._fontWeight == _fontWeight && other._fontStyle == _fontStyle && other._forceStrutHeight == _forceStrutHeight @@ -1259,6 +1264,7 @@ class EngineStrutStyle implements ui.StrutStyle { _fontSize, _height, _leading, + _leadingDistribution, _fontWeight, _fontStyle, _forceStrutHeight, diff --git a/lib/web_ui/lib/src/ui/text.dart b/lib/web_ui/lib/src/ui/text.dart index bffb948d5baa3..c94308e6369ab 100644 --- a/lib/web_ui/lib/src/ui/text.dart +++ b/lib/web_ui/lib/src/ui/text.dart @@ -204,18 +204,29 @@ enum TextDecorationStyle { wavy } +enum TextLeadingDistribution { + proportional, + even, +} + class TextHeightBehavior { const TextHeightBehavior({ this.applyHeightToFirstAscent = true, this.applyHeightToLastDescent = true, + this.leadingDistribution = TextLeadingDistribution.proportional, }); - const TextHeightBehavior.fromEncoded(int encoded) - : applyHeightToFirstAscent = (encoded & 0x1) == 0, - applyHeightToLastDescent = (encoded & 0x2) == 0; + TextHeightBehavior.fromEncoded(int encoded) + : applyHeightToFirstAscent = (encoded & 0x1) == 0, + applyHeightToLastDescent = (encoded & 0x2) == 0, + leadingDistribution = TextLeadingDistribution.values[encoded >> 2]; final bool applyHeightToFirstAscent; final bool applyHeightToLastDescent; + final TextLeadingDistribution leadingDistribution; + int encode() { - return (applyHeightToFirstAscent ? 0 : 1 << 0) | (applyHeightToLastDescent ? 0 : 1 << 1); + return (applyHeightToFirstAscent ? 0 : 1 << 0) + | (applyHeightToLastDescent ? 0 : 1 << 1) + | (leadingDistribution.index << 2); } @override @@ -224,7 +235,8 @@ class TextHeightBehavior { return false; return other is TextHeightBehavior && other.applyHeightToFirstAscent == applyHeightToFirstAscent - && other.applyHeightToLastDescent == applyHeightToLastDescent; + && other.applyHeightToLastDescent == applyHeightToLastDescent + && other.leadingDistribution == leadingDistribution; } @override @@ -239,7 +251,8 @@ class TextHeightBehavior { String toString() { return 'TextHeightBehavior(' 'applyHeightToFirstAscent: $applyHeightToFirstAscent, ' - 'applyHeightToLastDescent: $applyHeightToLastDescent' + 'applyHeightToLastDescent: $applyHeightToLastDescent, ' + 'leadingDistribution: $leadingDistribution' ')'; } } @@ -260,6 +273,7 @@ abstract class TextStyle { double? letterSpacing, double? wordSpacing, double? height, + TextLeadingDistribution? leadingDistribution, Locale? locale, Paint? background, Paint? foreground, @@ -370,6 +384,7 @@ abstract class StrutStyle { List? fontFamilyFallback, double? fontSize, double? height, + TextLeadingDistribution? leadingDistribution, double? leading, FontWeight? fontWeight, FontStyle? fontStyle, diff --git a/testing/dart/text_test.dart b/testing/dart/text_test.dart index 55ff04b42ed5c..9d4218e33db83 100644 --- a/testing/dart/text_test.dart +++ b/testing/dart/text_test.dart @@ -42,6 +42,37 @@ void main() { }); }); + group('TextStyle', () { + final TextStyle ts0 = TextStyle(fontWeight: FontWeight.w700, fontSize: 12.0, height: 123.0); + final TextStyle ts1 = TextStyle(color: const Color(0xFF00FF00), fontWeight: FontWeight.w800, fontSize: 10.0, height: 100.0); + final TextStyle ts2 = TextStyle(fontFamily: 'test'); + final TextStyle ts3 = TextStyle(fontFamily: 'foo', fontFamilyFallback: ['Roboto', 'test']); + final TextStyle ts4 = TextStyle(leadingDistribution: TextLeadingDistribution.even); + + test('toString works', () { + expect( + ts0.toString(), + equals('TextStyle(color: unspecified, decoration: unspecified, decorationColor: unspecified, decorationStyle: unspecified, decorationThickness: unspecified, fontWeight: FontWeight.w700, fontStyle: unspecified, textBaseline: unspecified, fontFamily: unspecified, fontFamilyFallback: unspecified, fontSize: 12.0, letterSpacing: unspecified, wordSpacing: unspecified, height: 123.0x, leadingDistribution: unspecified, locale: unspecified, background: unspecified, foreground: unspecified, shadows: unspecified, fontFeatures: unspecified)'), + ); + expect( + ts1.toString(), + equals('TextStyle(color: Color(0xff00ff00), decoration: unspecified, decorationColor: unspecified, decorationStyle: unspecified, decorationThickness: unspecified, fontWeight: FontWeight.w800, fontStyle: unspecified, textBaseline: unspecified, fontFamily: unspecified, fontFamilyFallback: unspecified, fontSize: 10.0, letterSpacing: unspecified, wordSpacing: unspecified, height: 100.0x, leadingDistribution: unspecified, locale: unspecified, background: unspecified, foreground: unspecified, shadows: unspecified, fontFeatures: unspecified)'), + ); + expect( + ts2.toString(), + equals('TextStyle(color: unspecified, decoration: unspecified, decorationColor: unspecified, decorationStyle: unspecified, decorationThickness: unspecified, fontWeight: unspecified, fontStyle: unspecified, textBaseline: unspecified, fontFamily: test, fontFamilyFallback: unspecified, fontSize: unspecified, letterSpacing: unspecified, wordSpacing: unspecified, height: unspecified, leadingDistribution: unspecified, locale: unspecified, background: unspecified, foreground: unspecified, shadows: unspecified, fontFeatures: unspecified)'), + ); + expect( + ts3.toString(), + equals('TextStyle(color: unspecified, decoration: unspecified, decorationColor: unspecified, decorationStyle: unspecified, decorationThickness: unspecified, fontWeight: unspecified, fontStyle: unspecified, textBaseline: unspecified, fontFamily: foo, fontFamilyFallback: [Roboto, test], fontSize: unspecified, letterSpacing: unspecified, wordSpacing: unspecified, height: unspecified, leadingDistribution: unspecified, locale: unspecified, background: unspecified, foreground: unspecified, shadows: unspecified, fontFeatures: unspecified)'), + ); + expect( + ts4.toString(), + equals('TextStyle(color: unspecified, decoration: unspecified, decorationColor: unspecified, decorationStyle: unspecified, decorationThickness: unspecified, fontWeight: unspecified, fontStyle: unspecified, textBaseline: unspecified, fontFamily: unspecified, fontFamilyFallback: unspecified, fontSize: unspecified, letterSpacing: unspecified, wordSpacing: unspecified, height: unspecified, leadingDistribution: TextLeadingDistribution.even, locale: unspecified, background: unspecified, foreground: unspecified, shadows: unspecified, fontFeatures: unspecified)'), + ); + }); + }); + group('TextHeightBehavior', () { const TextHeightBehavior behavior0 = TextHeightBehavior(); const TextHeightBehavior behavior1 = TextHeightBehavior( @@ -54,6 +85,10 @@ void main() { const TextHeightBehavior behavior3 = TextHeightBehavior( applyHeightToLastDescent: false ); + const TextHeightBehavior behavior4 = TextHeightBehavior( + applyHeightToLastDescent: false, + leadingDistribution: TextLeadingDistribution.even, + ); test('default constructor works', () { expect(behavior0.applyHeightToFirstAscent, equals(true)); @@ -67,6 +102,9 @@ void main() { expect(behavior3.applyHeightToFirstAscent, equals(true)); expect(behavior3.applyHeightToLastDescent, equals(false)); + + expect(behavior4.applyHeightToLastDescent, equals(false)); + expect(behavior4.leadingDistribution, equals(TextLeadingDistribution.even)); }); test('encode works', () { @@ -74,20 +112,23 @@ void main() { expect(behavior1.encode(), equals(3)); expect(behavior2.encode(), equals(1)); expect(behavior3.encode(), equals(2)); + expect(behavior4.encode(), equals(6)); }); test('decode works', () { - expect(const TextHeightBehavior.fromEncoded(0), equals(behavior0)); - expect(const TextHeightBehavior.fromEncoded(3), equals(behavior1)); - expect(const TextHeightBehavior.fromEncoded(1), equals(behavior2)); - expect(const TextHeightBehavior.fromEncoded(2), equals(behavior3)); + expect(TextHeightBehavior.fromEncoded(0), equals(behavior0)); + expect(TextHeightBehavior.fromEncoded(3), equals(behavior1)); + expect(TextHeightBehavior.fromEncoded(1), equals(behavior2)); + expect(TextHeightBehavior.fromEncoded(2), equals(behavior3)); + expect(TextHeightBehavior.fromEncoded(6), equals(behavior4)); }); test('toString works', () { - expect(behavior0.toString(), equals('TextHeightBehavior(applyHeightToFirstAscent: true, applyHeightToLastDescent: true)')); - expect(behavior1.toString(), equals('TextHeightBehavior(applyHeightToFirstAscent: false, applyHeightToLastDescent: false)')); - expect(behavior2.toString(), equals('TextHeightBehavior(applyHeightToFirstAscent: false, applyHeightToLastDescent: true)')); - expect(behavior3.toString(), equals('TextHeightBehavior(applyHeightToFirstAscent: true, applyHeightToLastDescent: false)')); + expect(behavior0.toString(), equals('TextHeightBehavior(applyHeightToFirstAscent: true, applyHeightToLastDescent: true, leadingDistribution: TextLeadingDistribution.proportional)')); + expect(behavior1.toString(), equals('TextHeightBehavior(applyHeightToFirstAscent: false, applyHeightToLastDescent: false, leadingDistribution: TextLeadingDistribution.proportional)')); + expect(behavior2.toString(), equals('TextHeightBehavior(applyHeightToFirstAscent: false, applyHeightToLastDescent: true, leadingDistribution: TextLeadingDistribution.proportional)')); + expect(behavior3.toString(), equals('TextHeightBehavior(applyHeightToFirstAscent: true, applyHeightToLastDescent: false, leadingDistribution: TextLeadingDistribution.proportional)')); + expect(behavior4.toString(), equals('TextHeightBehavior(applyHeightToFirstAscent: true, applyHeightToLastDescent: false, leadingDistribution: TextLeadingDistribution.even)')); }); }); diff --git a/third_party/txt/src/txt/paragraph_style.h b/third_party/txt/src/txt/paragraph_style.h index 46ea7a1e7c1a8..90c54ee4b23d8 100644 --- a/third_party/txt/src/txt/paragraph_style.h +++ b/third_party/txt/src/txt/paragraph_style.h @@ -41,10 +41,18 @@ enum class TextDirection { ltr, }; -// Allows disabling height adjustments to first line's ascent and the -// last line's descent. If disabled, the line will use the default font -// metric provided ascent/descent and ParagraphStyle.height will not take -// effect. +// Adjusts the leading over and under text. +// +// kDisableFirstAscent and kDisableLastDescent allow disabling height +// adjustments to first line's ascent and the last line's descent. If disabled, +// the line will use the default font metric provided ascent/descent and +// ParagraphStyle.height or TextStyle.height will not take effect. +// +// kEvenLeading determines how the leading is distributed over and under the +// text. When true, half of the leading is added to the top of the text and the +// other half is added to the bottom of the text. Otherwise, instead of +// distributing the space evenly, it's distributed proportionally to the font's +// ascent/descent ratio. // // The default behavior is kAll where height adjustments are enabled for all // lines. @@ -58,6 +66,7 @@ enum TextHeightBehavior { kDisableFirstAscent = 0x1, kDisableLastDescent = 0x2, kDisableAll = 0x1 | 0x2, + kEvenLeading = 0x1 << 2, }; class ParagraphStyle { @@ -69,8 +78,8 @@ class ParagraphStyle { std::string font_family = ""; double font_size = 14; double height = 1; - size_t text_height_behavior = TextHeightBehavior::kAll; bool has_height_override = false; + size_t text_height_behavior = TextHeightBehavior::kAll; // Strut properties. strut_enabled must be set to true for the rest of the // properties to take effect. @@ -82,6 +91,8 @@ class ParagraphStyle { double strut_font_size = 14; double strut_height = 1; bool strut_has_height_override = false; + bool strut_half_leading = false; + bool strut_has_leading_distribution_override = false; 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; diff --git a/third_party/txt/src/txt/paragraph_txt.cc b/third_party/txt/src/txt/paragraph_txt.cc index f1febacde5dd7..81f34ec8f42f0 100644 --- a/third_party/txt/src/txt/paragraph_txt.cc +++ b/third_party/txt/src/txt/paragraph_txt.cc @@ -518,8 +518,8 @@ bool ParagraphTxt::IsStrutValid() const { } void ParagraphTxt::ComputeStrut(StrutMetrics* strut, SkFont& font) { - strut->ascent = 0; - strut->descent = 0; + strut->ascent = std::numeric_limits::lowest(); + strut->descent = std::numeric_limits::lowest(); strut->leading = 0; strut->half_leading = 0; strut->line_height = 0; @@ -553,20 +553,34 @@ void ParagraphTxt::ComputeStrut(StrutMetrics* strut, SkFont& font) { SkFontMetrics strut_metrics; font.getMetrics(&strut_metrics); + const double metrics_height = + -strut_metrics.fAscent + strut_metrics.fDescent; + if (paragraph_style_.strut_has_height_override) { - double metrics_height = -strut_metrics.fAscent + strut_metrics.fDescent; - strut->ascent = (-strut_metrics.fAscent / metrics_height) * - paragraph_style_.strut_height * - paragraph_style_.strut_font_size; - strut->descent = (strut_metrics.fDescent / metrics_height) * - paragraph_style_.strut_height * - paragraph_style_.strut_font_size; - strut->leading = - // Zero leading if there is no user specified strut leading. + const double strut_height = + paragraph_style_.strut_height * paragraph_style_.strut_font_size; + const double metrics_leading = + // Zero extra leading if there is no user specified strut leading. paragraph_style_.strut_leading < 0 ? 0 : (paragraph_style_.strut_leading * paragraph_style_.strut_font_size); + + const bool half_leading_enabled = + paragraph_style_.strut_has_leading_distribution_override + ? paragraph_style_.strut_half_leading + : paragraph_style_.text_height_behavior & + TextHeightBehavior::kEvenLeading; + + const double available_height = + half_leading_enabled ? metrics_height : strut_height; + + strut->ascent = + (-strut_metrics.fAscent / metrics_height) * available_height; + strut->descent = + (strut_metrics.fDescent / metrics_height) * available_height; + + strut->leading = metrics_leading + strut_height - available_height; } else { strut->ascent = -strut_metrics.fAscent; strut->descent = strut_metrics.fDescent; @@ -671,8 +685,8 @@ void ParagraphTxt::Layout(double width) { glyph_lines_.clear(); code_unit_runs_.clear(); inline_placeholder_code_unit_runs_.clear(); - max_right_ = FLT_MIN; - min_left_ = FLT_MAX; + max_right_ = std::numeric_limits::lowest(); + min_left_ = std::numeric_limits::max(); final_line_count_ = 0; if (!ComputeLineBreaks()) @@ -1188,79 +1202,101 @@ void ParagraphTxt::UpdateLineMetrics(const SkFontMetrics& metrics, size_t line_number, size_t line_limit) { if (!strut_.force_strut) { - double ascent; - double descent; - if (style.has_height_override) { - // Scale the ascent and descent such that the sum of ascent and - // descent is `fontsize * style.height * style.font_size`. - // - // The raw metrics do not add up to fontSize. The state of font - // metrics is a mess: - // - // Each font has 4 sets of vertical metrics: - // - // * hhea: hheaAscender, hheaDescender, hheaLineGap. - // Used by Apple. - // * OS/2 typo: typoAscender, typoDescender, typoLineGap. - // Used sometimes by Windows for layout. - // * OS/2 win: winAscent, winDescent. - // Also used by Windows, generally will be cut if extends past - // these metrics. - // * EM Square: ascent, descent - // Not actively used, but this defines the 'scale' of the - // units used. - // - // `Use Typo Metrics` is a boolean that, when enabled, prefers - // typo metrics over win metrics. Default is off. Enabled by most - // modern fonts. - // - // In addition to these different sets of metrics, there are also - // multiple strategies for using these metrics: - // - // * Adobe: Set hhea values to typo equivalents. - // * Microsoft: Set hhea values to win equivalents. - // * Web: Use hhea values for text, regardless of `Use Typo Metrics` - // The hheaLineGap is distributed half across the top and half - // across the bottom of the line. - // Exceptions: - // Windows: All browsers respect `Use Typo Metrics` - // Firefox respects `Use Typo Metrics`. - // - // This pertains to this code in that it is ambiguous which set of - // metrics we are actually using via SkFontMetrics. This in turn - // means that if we use the raw metrics, we will see differences - // between platforms as well as unpredictable line heights. - // - // A more thorough explanation is available at - // https://glyphsapp.com/tutorials/vertical-metrics - // - // Doing this ascent/descent normalization to the EM Square allows - // a sane, consistent, and reasonable line height to be specified, - // though it breaks with what is done by any of the platforms above. - double metrics_height = -metrics.fAscent + metrics.fDescent; - ascent = - (-metrics.fAscent / metrics_height) * style.height * style.font_size; - descent = - (metrics.fDescent / metrics_height) * style.height * style.font_size; - } else { - // Use the font-provided ascent, descent, and leading directly. - ascent = (-metrics.fAscent + metrics.fLeading / 2); - descent = (metrics.fDescent + metrics.fLeading / 2); - } - - // Account for text_height_behavior in paragraph_style_. + const double metrics_font_height = metrics.fDescent - metrics.fAscent; + // The overall height of the glyph blob. If neither the ascent or the + // descent is disabled, we have block_height = ascent + descent, where + // "ascent" is the extent from the top of the blob to its baseline, and + // "descent" is the extent from the text blob's baseline to its bottom. Not + // to be mistaken with the font's ascent and descent. + const double blob_height = style.has_height_override + ? style.height * style.font_size + : metrics_font_height + metrics.fLeading; + const bool half_leading_enabled = + style.has_leading_distribution_override + ? style.half_leading + : paragraph_style_.text_height_behavior & + TextHeightBehavior::kEvenLeading; + + // Scale the ascent and descent such that the sum of ascent and + // descent is `style.height * style.font_size`. // - // Disable first line ascent modifications. - if (line_number == 0 && paragraph_style_.text_height_behavior & - TextHeightBehavior::kDisableFirstAscent) { - ascent = -metrics.fAscent; - } - // Disable last line descent modifications. - if (line_number == line_limit - 1 && - paragraph_style_.text_height_behavior & - TextHeightBehavior::kDisableLastDescent) { - descent = metrics.fDescent; + // The raw metrics do not add up to fontSize. The state of font + // metrics is a mess: + // + // Each font has 4 sets of vertical metrics: + // + // * hhea: hheaAscender, hheaDescender, hheaLineGap. + // Used by Apple. + // * OS/2 typo: typoAscender, typoDescender, typoLineGap. + // Used sometimes by Windows for layout. + // * OS/2 win: winAscent, winDescent. + // Also used by Windows, generally will be cut if extends past + // these metrics. + // * EM Square: ascent, descent + // Not actively used, but this defines the 'scale' of the + // units used. + // + // `Use Typo Metrics` is a boolean that, when enabled, prefers + // typo metrics over win metrics. Default is off. Enabled by most + // modern fonts. + // + // In addition to these different sets of metrics, there are also + // multiple strategies for using these metrics: + // + // * Adobe: Set hhea values to typo equivalents. + // * Microsoft: Set hhea values to win equivalents. + // * Web: Use hhea values for text, regardless of `Use Typo Metrics` + // The hheaLineGap is distributed half across the top and half + // across the bottom of the line. + // Exceptions: + // Windows: All browsers respect `Use Typo Metrics` + // Firefox respects `Use Typo Metrics`. + // + // This pertains to this code in that it is ambiguous which set of + // metrics we are actually using via SkFontMetrics. This in turn + // means that if we use the raw metrics, we will see differences + // between platforms as well as unpredictable line heights. + // + // A more thorough explanation is available at + // https://glyphsapp.com/tutorials/vertical-metrics + // + // Doing this ascent/descent normalization to the EM Square allows + // a sane, consistent, and reasonable "blob_height" to be specified, + // though it breaks with what is done by any of the platforms above. + const bool shouldNormalizeFont = + style.has_height_override && !half_leading_enabled; + const double font_height = + shouldNormalizeFont ? style.font_size : metrics_font_height; + + // Reserve the outermost vertical space we want to distribute evenly over + // and under the text ("half-leading"). + double leading; + if (half_leading_enabled) { + leading = blob_height - font_height; + } else { + leading = style.has_height_override ? 0.0 : metrics.fLeading; } + const double half_leading = leading / 2; + + // Proportionally distribute the remaining vertical space above and below + // the glyph blob's baseline, per the font's ascent/discent ratio. + const double available_vspace = blob_height - leading; + const double modifiedAscent = + -metrics.fAscent / metrics_font_height * available_vspace + + half_leading; + const double modifiedDescent = + metrics.fDescent / metrics_font_height * available_vspace + + half_leading; + + const bool disableAscent = + line_number == 0 && paragraph_style_.text_height_behavior & + TextHeightBehavior::kDisableFirstAscent; + const bool disableDescent = line_number == line_limit - 1 && + paragraph_style_.text_height_behavior & + TextHeightBehavior::kDisableLastDescent; + + double ascent = disableAscent ? -metrics.fAscent : modifiedAscent; + double descent = disableDescent ? metrics.fDescent : modifiedDescent; ComputePlaceholder(placeholder_run, ascent, descent); @@ -1644,8 +1680,8 @@ std::vector ParagraphTxt::GetRectsForRange( // Per-line metrics for max and min coordinates for left and right boxes. // These metrics cannot be calculated in layout generically because of // selections that do not cover the whole line. - SkScalar max_right = FLT_MIN; - SkScalar min_left = FLT_MAX; + SkScalar max_right = std::numeric_limits::lowest(); + SkScalar min_left = std::numeric_limits::max(); }; std::map line_box_metrics; @@ -1922,8 +1958,8 @@ std::vector ParagraphTxt::GetRectsForPlaceholders() { // Per-line metrics for max and min coordinates for left and right boxes. // These metrics cannot be calculated in layout generically because of // selections that do not cover the whole line. - SkScalar max_right = FLT_MIN; - SkScalar min_left = FLT_MAX; + SkScalar max_right = std::numeric_limits::lowest(); + SkScalar min_left = std::numeric_limits::max(); }; std::vector boxes; diff --git a/third_party/txt/src/txt/paragraph_txt.h b/third_party/txt/src/txt/paragraph_txt.h index 0919130868211..e0cf8a6ddd6d0 100644 --- a/third_party/txt/src/txt/paragraph_txt.h +++ b/third_party/txt/src/txt/paragraph_txt.h @@ -315,8 +315,8 @@ class ParagraphTxt : public Paragraph { double longest_line_ = -1.0f; double max_intrinsic_width_ = 0; double min_intrinsic_width_ = 0; - double alphabetic_baseline_ = FLT_MAX; - double ideographic_baseline_ = FLT_MAX; + double alphabetic_baseline_ = std::numeric_limits::max(); + double ideographic_baseline_ = std::numeric_limits::max(); bool needs_layout_ = true; diff --git a/third_party/txt/src/txt/text_style.cc b/third_party/txt/src/txt/text_style.cc index 04589659b6539..b520c0f0dfa2f 100644 --- a/third_party/txt/src/txt/text_style.cc +++ b/third_party/txt/src/txt/text_style.cc @@ -48,6 +48,11 @@ bool TextStyle::equals(const TextStyle& other) const { return false; if (has_height_override != other.has_height_override) return false; + if (has_leading_distribution_override != + other.has_leading_distribution_override) + return false; + if (half_leading != other.half_leading) + return false; if (locale != other.locale) return false; if (foreground != other.foreground) diff --git a/third_party/txt/src/txt/text_style.h b/third_party/txt/src/txt/text_style.h index 0d4a92a099814..6d6bd34766343 100644 --- a/third_party/txt/src/txt/text_style.h +++ b/third_party/txt/src/txt/text_style.h @@ -44,6 +44,10 @@ class TextStyle { FontWeight font_weight = FontWeight::w400; FontStyle font_style = FontStyle::normal; TextBaseline text_baseline = TextBaseline::kAlphabetic; + bool half_leading = false; + // whether TextStyle.half_leading should be applied or we should defer to + // ParagraphStyle.half_leading. + bool has_leading_distribution_override = false; // An ordered list of fonts in order of priority. The first font is more // highly preferred than the last font. std::vector font_families; diff --git a/third_party/txt/tests/paragraph_unittests.cc b/third_party/txt/tests/paragraph_unittests.cc index 445202b60ef7c..81e2f64113345 100644 --- a/third_party/txt/tests/paragraph_unittests.cc +++ b/third_party/txt/tests/paragraph_unittests.cc @@ -1883,6 +1883,491 @@ TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(HeightOverrideParagraph)) { ASSERT_TRUE(Snapshot()); } +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(HeightOverrideHalfLeadingParagraph)) { + // All 3 lines will have the same typeface. + 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.text_height_behavior = TextHeightBehavior::kEvenLeading; + txt::ParagraphBuilderTxt builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 20; + text_style.letter_spacing = 0; + text_style.word_spacing = 0; + text_style.color = SK_ColorBLACK; + text_style.height = 3.6345; + text_style.has_height_override = true; + // Disables text style leading distribution behavior override so it defaults + // to the paragraph style. + text_style.has_leading_distribution_override = false; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = BuildParagraph(builder); + 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::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, 40, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + // With half-leadding, the x coordinates should remain the same but the glyphs + // are shifted up (as compared to AD-scaling). + EXPECT_EQ(boxes.size(), 3ull); + + const double line_spacing1 = boxes[1].rect.top() - boxes[0].rect.bottom(); + const double line_spacing2 = boxes[2].rect.top() - boxes[1].rect.bottom(); + + EXPECT_EQ(line_spacing1, line_spacing2); + // half leading. + EXPECT_NEAR(line_spacing1 * 0.5, boxes[0].rect.top(), 0.5); + + EXPECT_FLOAT_EQ(boxes[1].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[1].rect.right(), 43.851562); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(HeightOverrideHalfLeadingTextStyle)) { + // All 3 lines will have the same typeface. + 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.text_height_behavior = TextHeightBehavior::kAll; + txt::ParagraphBuilderTxt builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 20; + text_style.letter_spacing = 0; + text_style.word_spacing = 0; + text_style.color = SK_ColorBLACK; + text_style.height = 3.6345; + text_style.has_height_override = true; + // Override paragraph_style.text_height_behavior: + text_style.has_leading_distribution_override = true; + text_style.half_leading = true; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = BuildParagraph(builder); + 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_style_max = + Paragraph::RectHeightStyle::kMax; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + + std::vector boxes = + paragraph->GetRectsForRange(0, 40, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + + std::vector line_boxes = paragraph->GetRectsForRange( + 0, 40, rect_height_style_max, rect_width_style); + EXPECT_EQ(boxes.size(), 3ull); + EXPECT_EQ(line_boxes.size(), 3ull); + + const double line_spacing1 = boxes[1].rect.top() - boxes[0].rect.bottom(); + const double line_spacing2 = boxes[2].rect.top() - boxes[1].rect.bottom(); + + EXPECT_EQ(line_spacing1, line_spacing2); + + // half leading. + EXPECT_EQ(line_boxes[0].rect.top() - boxes[0].rect.top(), + boxes[0].rect.bottom() - line_boxes[0].rect.bottom()); + EXPECT_EQ(line_boxes[1].rect.top() - boxes[1].rect.top(), + boxes[1].rect.bottom() - line_boxes[1].rect.bottom()); + EXPECT_EQ(line_boxes[2].rect.top() - boxes[2].rect.top(), + boxes[2].rect.bottom() - line_boxes[2].rect.bottom()); + // With half-leadding, the x coordinates should remain the same. + EXPECT_FLOAT_EQ(boxes[1].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[1].rect.right(), 43.851562); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(MixedTextHeightBehaviorSameLine)) { + // Both runs will still have the same typeface, but with different text height + // behaviors. + const char* text = "01234満毎冠行来昼本可abcd"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + std::u16string u16_text2(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 10; + paragraph_style.text_height_behavior = TextHeightBehavior::kAll; + txt::ParagraphBuilderTxt builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 20; + text_style.letter_spacing = 0; + text_style.word_spacing = 0; + text_style.color = SK_ColorBLACK; + text_style.height = 3.6345; + text_style.has_height_override = true; + // First run, with half-leading. + text_style.has_leading_distribution_override = true; + text_style.half_leading = true; + builder.PushStyle(text_style); + builder.AddText(u16_text); + + // Second run with AD-scaling. + text_style.has_leading_distribution_override = true; + text_style.half_leading = false; + + builder.PushStyle(text_style); + builder.AddText(u16_text2); + builder.Pop(); + + auto paragraph = BuildParagraph(builder); + 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_style_max = + Paragraph::RectHeightStyle::kMax; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + + std::vector boxes = paragraph->GetRectsForRange( + 0, icu_text.length(), rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + + std::vector line_boxes = paragraph->GetRectsForRange( + 0, icu_text.length(), rect_height_style_max, rect_width_style); + // The runs has the same typeface so they should be grouped together. + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_EQ(line_boxes.size(), 1ull); + + const double glyphHeight = boxes[0].rect.height(); + const double metricsAscent = 18.5546875; + const double metricsDescent = 4.8828125; + EXPECT_DOUBLE_EQ(glyphHeight, metricsAscent + metricsDescent); + + const double line_height = 3.6345 * 20; + const double leading = line_height - glyphHeight; + + // Overall descent is from half-leading and overall ascent is from AD-scaling. + EXPECT_NEAR(boxes[0].rect.top() - line_boxes[0].rect.top(), + leading * metricsAscent / (metricsAscent + metricsDescent), + 0.001); + + EXPECT_NEAR(line_boxes[0].rect.bottom() - boxes[0].rect.bottom(), + leading * 0.5, 0.001); +} + +TEST_F(ParagraphTest, + DISABLE_ON_WINDOWS(MixedTextHeightBehaviorSameLineWithZeroHeight)) { + // Both runs will still have the same typeface, but with different text height + // behaviors. + const char* text = "01234満毎冠行来昼本可abcd"; + 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.text_height_behavior = TextHeightBehavior::kAll; + txt::ParagraphBuilderTxt builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 20; + text_style.letter_spacing = 0; + text_style.word_spacing = 0; + text_style.color = SK_ColorBLACK; + // Set height to 0 + text_style.height = 0; + text_style.has_height_override = true; + // First run, with half-leading. + text_style.has_leading_distribution_override = true; + text_style.half_leading = true; + builder.PushStyle(text_style); + builder.AddText(u16_text); + + // Second run with AD-scaling. + text_style.has_leading_distribution_override = true; + text_style.half_leading = false; + + builder.PushStyle(text_style); + builder.AddText(u16_text); + builder.Pop(); + + auto paragraph = BuildParagraph(builder); + 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_style_max = + Paragraph::RectHeightStyle::kMax; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + + std::vector boxes = paragraph->GetRectsForRange( + 0, icu_text.length(), rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + + std::vector line_boxes = paragraph->GetRectsForRange( + 0, icu_text.length(), rect_height_style_max, rect_width_style); + // The runs has the same typeface so they should be grouped together. + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_EQ(line_boxes.size(), 1ull); + + const double glyphHeight = boxes[0].rect.height(); + const double metricsAscent = 18.5546875; + const double metricsDescent = 4.8828125; + EXPECT_DOUBLE_EQ(glyphHeight, metricsAscent + metricsDescent); + + // line_height for both styled runs is 0, but the overall line height is not + // 0. + EXPECT_DOUBLE_EQ(line_boxes[0].rect.height(), + metricsAscent - (metricsAscent + metricsDescent) / 2); + EXPECT_LT(boxes[0].rect.top(), 0.0); + EXPECT_GT(boxes[0].rect.bottom(), 0.0); +} + +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(HeightOverrideHalfLeadingStrut)) { + // All 3 lines will have the same typeface. + 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_enabled = true; + paragraph_style.strut_has_height_override = true; + paragraph_style.strut_height = 3.6345; + paragraph_style.strut_font_size = 20; + paragraph_style.strut_font_families.push_back("Roboto"); + paragraph_style.strut_has_leading_distribution_override = true; + paragraph_style.strut_half_leading = true; + txt::ParagraphBuilderTxt builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 20; + text_style.letter_spacing = 0; + text_style.word_spacing = 0; + text_style.color = SK_ColorBLACK; + text_style.height = 3.6345; + text_style.has_height_override = true; + // Override paragraph_style.text_height_behavior: + text_style.has_leading_distribution_override = true; + text_style.half_leading = true; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = BuildParagraph(builder); + 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_style_max = + Paragraph::RectHeightStyle::kMax; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + + std::vector boxes = + paragraph->GetRectsForRange(0, 40, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + + std::vector line_boxes = paragraph->GetRectsForRange( + 0, 40, rect_height_style_max, rect_width_style); + EXPECT_EQ(boxes.size(), 3ull); + EXPECT_EQ(line_boxes.size(), 3ull); + + const double line_spacing1 = boxes[1].rect.top() - boxes[0].rect.bottom(); + const double line_spacing2 = boxes[2].rect.top() - boxes[1].rect.bottom(); + + EXPECT_EQ(line_spacing1, line_spacing2); + + // Strut half leading. + EXPECT_EQ(line_boxes[0].rect.top() - boxes[0].rect.top(), + boxes[0].rect.bottom() - line_boxes[0].rect.bottom()); + EXPECT_EQ(line_boxes[1].rect.top() - boxes[1].rect.top(), + boxes[1].rect.bottom() - line_boxes[1].rect.bottom()); + EXPECT_EQ(line_boxes[2].rect.top() - boxes[2].rect.top(), + boxes[2].rect.bottom() - line_boxes[2].rect.bottom()); + + EXPECT_FLOAT_EQ(boxes[1].rect.left(), 0); + EXPECT_FLOAT_EQ(boxes[1].rect.right(), 43.851562); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, + DISABLE_ON_WINDOWS(ZeroHeightHalfLeadingStrutForceHeight)) { + // All 3 lines will have the same typeface. + const char* text = "01234満毎冠行来昼本可abcdn満毎冠行来昼本可"; + 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_enabled = true; + paragraph_style.strut_has_height_override = true; + paragraph_style.strut_height = 0; + // Force strut height. + paragraph_style.force_strut_height = true; + paragraph_style.strut_font_size = 20; + paragraph_style.strut_font_families.push_back("Roboto"); + paragraph_style.strut_half_leading = true; + txt::ParagraphBuilderTxt builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 20; + text_style.letter_spacing = 0; + text_style.word_spacing = 0; + text_style.color = SK_ColorBLACK; + text_style.height = 0; + text_style.has_height_override = true; + + // First run, with half-leading. + text_style.has_leading_distribution_override = true; + text_style.half_leading = true; + builder.PushStyle(text_style); + builder.AddText(u16_text); + + // Second run with AD-scaling. + text_style.has_leading_distribution_override = true; + text_style.half_leading = false; + + builder.PushStyle(text_style); + builder.AddText(u16_text); + builder.Pop(); + + auto paragraph = BuildParagraph(builder); + 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_style_max = + Paragraph::RectHeightStyle::kMax; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + + std::vector boxes = paragraph->GetRectsForRange( + 0, icu_text.length(), rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + + std::vector line_boxes = paragraph->GetRectsForRange( + 0, icu_text.length(), rect_height_style_max, rect_width_style); + // The runs has the same typeface so they should be grouped together. + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_EQ(line_boxes.size(), 1ull); + + const double glyphHeight = boxes[0].rect.height(); + const double metricsAscent = 18.5546875; + const double metricsDescent = 4.8828125; + EXPECT_DOUBLE_EQ(glyphHeight, metricsAscent + metricsDescent); + + EXPECT_DOUBLE_EQ(line_boxes[0].rect.height(), 0.0); + + ASSERT_TRUE(Snapshot()); +} + TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(LeftAlignParagraph)) { const char* text = "This is a very long sentence to test if the text will properly wrap " @@ -6609,4 +7094,68 @@ TEST_F(ParagraphTest, TextHeightBehaviorRectsParagraph) { ASSERT_TRUE(Snapshot()); } +TEST_F(ParagraphTest, MixedTextHeightBehaviorRectsParagraph) { + const char* text = "0123456789"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + // The paragraph's first line and the last line use the font's ascent/descent. + paragraph_style.text_height_behavior = + txt::TextHeightBehavior::kDisableFirstAscent | + txt::TextHeightBehavior::kDisableLastDescent; + + txt::ParagraphBuilderTxt builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.color = SK_ColorBLACK; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 30; + text_style.height = 5; + text_style.has_height_override = true; + text_style.has_leading_distribution_override = true; + text_style.half_leading = true; + + builder.PushStyle(text_style); + builder.AddText(u16_text); + + text_style.half_leading = false; + builder.PushStyle(text_style); + builder.AddText(u16_text); + + // 2 identical runs except the first run has half-leading enabled. + builder.Pop(); + + auto paragraph = BuildParagraph(builder); + paragraph->Layout(GetTestCanvasWidth() - 300); + + 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::kMax; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForRange(0, 20, rect_height_style, rect_width_style); + + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + // The kDisableAll flag is applied. + EXPECT_GT(boxes.size(), 1ull); + // The height of the line equals to the metrics height of the font + // (ascent + descent). + EXPECT_FLOAT_EQ(boxes[0].rect.bottom() - boxes[0].rect.top(), + 27.8320312 + 7.32421875); + + ASSERT_TRUE(Snapshot()); +} } // namespace txt