diff --git a/lib/ui/text.dart b/lib/ui/text.dart index f77ea978f9cfd..86ca83f789471 100644 --- a/lib/ui/text.dart +++ b/lib/ui/text.dart @@ -933,6 +933,67 @@ class FontFeature { String toString() => "FontFeature('$feature', $value)"; } +/// An axis tag and value that can be used to customize variable fonts. +/// +/// Some fonts are variable fonts that can generate a range of different +/// font faces by altering the values of the font's design axes. +/// +/// See https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview +/// +/// Example: +/// `TextStyle(fontVariations: [FontVariation('wght', 800.0)])` +class FontVariation { + /// Creates a [FontVariation] object, which can be added to a [TextStyle] to + /// change the variable attributes of a font. + /// + /// `axis` is the four-character tag that identifies the design axis. + /// These tags are specified by font formats such as OpenType. + /// See https://docs.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg + /// + /// `value` is the value that the axis will be set to. The behavior + /// depends on how the font implements the axis. + const FontVariation( + this.axis, + this.value, + ) : assert(axis != null), + assert(axis.length == 4, 'Axis tag must be exactly four characters long.'), + assert(value != null); + + /// The tag that identifies the design axis. Must consist of 4 ASCII + /// characters. + final String axis; + + /// The value assigned to this design axis. + /// + /// The range of usable values depends on the specification of the axis. + final double value; + + static const int _kEncodedSize = 8; + + void _encode(ByteData byteData) { + assert(axis.codeUnits.every((int c) => c >= 0x20 && c <= 0x7F)); + for (int i = 0; i < 4; i++) { + byteData.setUint8(i, axis.codeUnitAt(i)); + } + byteData.setFloat32(4, value, _kFakeHostEndian); + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) + return false; + return other is FontVariation + && other.axis == axis + && other.value == value; + } + + @override + int get hashCode => hashValues(axis, value); + + @override + String toString() => "FontVariation('$axis', $value)"; +} + /// Whether and how to align text horizontally. // The order of this enum must match the order of the values in RenderStyleConstants.h's ETextAlign. enum TextAlign { @@ -1255,6 +1316,7 @@ Int32List _encodeTextStyle( Paint? foreground, List? shadows, List? fontFeatures, + List? fontVariations, ) { final Int32List result = Int32List(9); // The 0th bit of result[0] is reserved for leadingDistribution. @@ -1330,6 +1392,10 @@ Int32List _encodeTextStyle( result[0] |= 1 << 18; // Passed separately to native. } + if (fontVariations != null) { + result[0] |= 1 << 19; + // Passed separately to native. + } return result; } @@ -1371,6 +1437,7 @@ class TextStyle { /// * `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. /// * `fontFeatures`: The font features that should be applied to the text. + /// * `fontVariations`: The font variations that should be applied to the text. TextStyle({ Color? color, TextDecoration? decoration, @@ -1392,6 +1459,7 @@ class TextStyle { Paint? foreground, List? shadows, List? fontFeatures, + List? fontVariations, }) : assert(color == null || foreground == null, 'Cannot provide both a color and a foreground\n' 'The color argument is just a shorthand for "foreground: Paint()..color = color".' @@ -1416,6 +1484,7 @@ class TextStyle { foreground, shadows, fontFeatures, + fontVariations, ), _leadingDistribution = leadingDistribution, _fontFamily = fontFamily ?? '', @@ -1429,7 +1498,8 @@ class TextStyle { _background = background, _foreground = foreground, _shadows = shadows, - _fontFeatures = fontFeatures; + _fontFeatures = fontFeatures, + _fontVariations = fontVariations; final Int32List _encoded; final String _fontFamily; @@ -1444,6 +1514,7 @@ class TextStyle { final Paint? _foreground; final List? _shadows; final List? _fontFeatures; + final List? _fontVariations; final TextLeadingDistribution? _leadingDistribution; @override @@ -1464,11 +1535,12 @@ class TextStyle { && _listEquals(other._encoded, _encoded) && _listEquals(other._shadows, _shadows) && _listEquals(other._fontFamilyFallback, _fontFamilyFallback) - && _listEquals(other._fontFeatures, _fontFeatures); + && _listEquals(other._fontFeatures, _fontFeatures) + && _listEquals(other._fontVariations, _fontVariations); } @override - int get hashCode => hashValues(hashList(_encoded), _leadingDistribution, _fontFamily, _fontFamilyFallback, _fontSize, _letterSpacing, _wordSpacing, _height, _locale, _background, _foreground, hashList(_shadows), _decorationThickness, hashList(_fontFeatures)); + int get hashCode => hashValues(hashList(_encoded), _leadingDistribution, _fontFamily, _fontFamilyFallback, _fontSize, _letterSpacing, _wordSpacing, _height, _locale, _background, _foreground, hashList(_shadows), _decorationThickness, hashList(_fontFeatures), hashList(_fontVariations)); @override String toString() { @@ -1496,7 +1568,8 @@ class TextStyle { '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"}' + 'fontFeatures: ${ _encoded[0] & 0x40000 == 0x40000 ? _fontFeatures : "unspecified"}, ' + 'fontVariations: ${ _encoded[0] & 0x80000 == 0x80000 ? _fontVariations : "unspecified"}' ')'; } } @@ -2868,6 +2941,17 @@ class ParagraphBuilder extends NativeFieldWrapperClass1 { } } + ByteData? encodedFontVariations; + final List? fontVariations = style._fontVariations; + if (fontVariations != null) { + encodedFontVariations = ByteData(fontVariations.length * FontVariation._kEncodedSize); + int byteOffset = 0; + for (final FontVariation variation in fontVariations) { + variation._encode(ByteData.view(encodedFontVariations.buffer, byteOffset, FontVariation._kEncodedSize)); + byteOffset += FontVariation._kEncodedSize; + } + } + _pushStyle( encoded, fullFontFamilies, @@ -2883,6 +2967,7 @@ class ParagraphBuilder extends NativeFieldWrapperClass1 { style._foreground?._data, Shadow._encodeShadows(style._shadows), encodedFontFeatures, + encodedFontVariations, ); } @@ -2901,6 +2986,7 @@ class ParagraphBuilder extends NativeFieldWrapperClass1 { ByteData? foregroundData, ByteData shadowsData, ByteData? fontFeaturesData, + ByteData? fontVariationsData, ) native 'ParagraphBuilder_pushStyle'; static String _encodeLocale(Locale? locale) => locale?.toString() ?? ''; diff --git a/lib/ui/text/paragraph_builder.cc b/lib/ui/text/paragraph_builder.cc index 793af6601c699..d4cd88bb57691 100644 --- a/lib/ui/text/paragraph_builder.cc +++ b/lib/ui/text/paragraph_builder.cc @@ -51,6 +51,7 @@ const int tsBackgroundIndex = 15; const int tsForegroundIndex = 16; const int tsTextShadowsIndex = 17; const int tsFontFeaturesIndex = 18; +const int tsFontVariationsIndex = 19; const int tsLeadingDistributionMask = 1 << tsLeadingDistributionIndex; const int tsColorMask = 1 << tsColorIndex; @@ -71,6 +72,7 @@ const int tsBackgroundMask = 1 << tsBackgroundIndex; const int tsForegroundMask = 1 << tsForegroundIndex; const int tsTextShadowsMask = 1 << tsTextShadowsIndex; const int tsFontFeaturesMask = 1 << tsFontFeaturesIndex; +const int tsFontVariationsMask = 1 << tsFontVariationsIndex; // ParagraphStyle @@ -114,6 +116,10 @@ constexpr uint32_t kBlurOffset = 3; constexpr uint32_t kBytesPerFontFeature = 8; constexpr uint32_t kFontFeatureTagLength = 4; +// FontVariation decoding +constexpr uint32_t kBytesPerFontVariation = 8; +constexpr uint32_t kFontVariationTagLength = 4; + // Strut decoding const int sFontWeightIndex = 0; const int sFontStyleIndex = 1; @@ -365,6 +371,24 @@ void decodeFontFeatures(Dart_Handle font_features_data, } } +void decodeFontVariations(Dart_Handle font_variations_data, + txt::FontVariations& font_variations) { // NOLINT + tonic::DartByteData byte_data(font_variations_data); + FML_CHECK(byte_data.length_in_bytes() % kBytesPerFontVariation == 0); + + size_t variation_count = byte_data.length_in_bytes() / kBytesPerFontVariation; + for (size_t variation_index = 0; variation_index < variation_count; + ++variation_index) { + size_t variation_offset = variation_index * kBytesPerFontVariation; + const char* variation_bytes = + static_cast(byte_data.data()) + variation_offset; + std::string tag(variation_bytes, kFontVariationTagLength); + float value = *(reinterpret_cast(variation_bytes + + kFontVariationTagLength)); + font_variations.SetAxisValue(tag, value); + } +} + void ParagraphBuilder::pushStyle(tonic::Int32List& encoded, const std::vector& fontFamilies, double fontSize, @@ -378,7 +402,8 @@ void ParagraphBuilder::pushStyle(tonic::Int32List& encoded, Dart_Handle foreground_objects, Dart_Handle foreground_data, Dart_Handle shadows_data, - Dart_Handle font_features_data) { + Dart_Handle font_features_data, + Dart_Handle font_variations_data) { FML_DCHECK(encoded.num_elements() == 9); int32_t mask = encoded[0]; @@ -483,6 +508,10 @@ void ParagraphBuilder::pushStyle(tonic::Int32List& encoded, decodeFontFeatures(font_features_data, style.font_features); } + if (mask & tsFontVariationsMask) { + decodeFontVariations(font_variations_data, style.font_variations); + } + m_paragraphBuilder->PushStyle(style); } diff --git a/lib/ui/text/paragraph_builder.h b/lib/ui/text/paragraph_builder.h index 01baed167f9b0..c372a3c354a20 100644 --- a/lib/ui/text/paragraph_builder.h +++ b/lib/ui/text/paragraph_builder.h @@ -51,7 +51,8 @@ class ParagraphBuilder : public RefCountedDartWrappable { Dart_Handle foreground_objects, Dart_Handle foreground_data, Dart_Handle shadows_data, - Dart_Handle font_features_data); + Dart_Handle font_features_data, + Dart_Handle font_variations_data); void pop(); diff --git a/lib/web_ui/lib/text.dart b/lib/web_ui/lib/text.dart index c54f4d35ac0fe..69e88c3dc54a1 100644 --- a/lib/web_ui/lib/text.dart +++ b/lib/web_ui/lib/text.dart @@ -179,6 +179,33 @@ class FontFeature { String toString() => "FontFeature('$feature', $value)"; } +class FontVariation { + const FontVariation( + this.axis, + this.value, + ) : assert(axis != null), + assert(axis.length == 4, 'Axis tag must be exactly four characters long.'), + assert(value != null); + + final String axis; + final double value; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) + return false; + return other is FontVariation + && other.axis == axis + && other.value == value; + } + + @override + int get hashCode => hashValues(axis, value); + + @override + String toString() => "FontVariation('$axis', $value)"; +} + // The order of this enum must match the order of the values in RenderStyleConstants.h's ETextAlign. enum TextAlign { left, @@ -312,6 +339,9 @@ abstract class TextStyle { Paint? foreground, List? shadows, List? fontFeatures, + // TODO(jsimmons): implement fontVariations for web + // ignore: avoid_unused_constructor_parameters + List? fontVariations, }) { if (engine.useCanvasKit) { return engine.CkTextStyle( diff --git a/testing/dart/text_test.dart b/testing/dart/text_test.dart index dbedd93df81e5..656432193f716 100644 --- a/testing/dart/text_test.dart +++ b/testing/dart/text_test.dart @@ -2,11 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; import 'package:litetest/litetest.dart'; +import 'package:path/path.dart' as path; + +Future readFile(String fileName) async { + final File file = File(path.join('flutter', 'testing', 'resources', fileName)); + return file.readAsBytes(); +} void testFontWeightLerp() { test('FontWeight.lerp works with non-null values', () { @@ -50,23 +58,23 @@ void testTextStyle() { test('TextStyle 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)'), + 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, fontVariations: 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)'), + 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, fontVariations: 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)'), + 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, fontVariations: 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)'), + 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, fontVariations: 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)'), + 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, fontVariations: unspecified)'), ); }); } @@ -233,6 +241,38 @@ void testFontFeatureClass() { }); } +void testFontVariation() { + test('FontVariation', () async { + final Uint8List fontData = await readFile('RobotoSlab-VariableFont_wght.ttf'); + await loadFontFromList(fontData, fontFamily: 'RobotoSerif'); + + final ParagraphBuilder baseBuilder = ParagraphBuilder(ParagraphStyle( + fontFamily: 'RobotoSerif', + fontSize: 40.0, + )); + baseBuilder.addText('Hello'); + final Paragraph baseParagraph = baseBuilder.build(); + baseParagraph.layout(ParagraphConstraints(width: double.infinity)); + final double baseWidth = baseParagraph.minIntrinsicWidth; + + final ParagraphBuilder wideBuilder = ParagraphBuilder(ParagraphStyle( + fontFamily: 'RobotoSerif', + fontSize: 40.0, + )); + wideBuilder.pushStyle(TextStyle( + fontFamily: 'RobotoSerif', + fontSize: 40.0, + fontVariations: [FontVariation('wght', 900.0)] + )); + wideBuilder.addText('Hello'); + final Paragraph wideParagraph = wideBuilder.build(); + wideParagraph.layout(ParagraphConstraints(width: double.infinity)); + final double wideWidth = wideParagraph.minIntrinsicWidth; + + expect(wideWidth, greaterThan(baseWidth)); + }); +} + void main() { testFontWeightLerp(); testParagraphStyle(); @@ -241,4 +281,5 @@ void main() { testTextRange(); testLoadFontFromList(); testFontFeatureClass(); + testFontVariation(); } diff --git a/testing/resources/RobotoSlab-VariableFont_wght.ttf b/testing/resources/RobotoSlab-VariableFont_wght.ttf new file mode 100644 index 0000000000000..b14200ebcb3a0 Binary files /dev/null and b/testing/resources/RobotoSlab-VariableFont_wght.ttf differ diff --git a/third_party/txt/src/skia/paragraph_builder_skia.cc b/third_party/txt/src/skia/paragraph_builder_skia.cc index 84799c3838737..2e2533954b71a 100644 --- a/third_party/txt/src/skia/paragraph_builder_skia.cc +++ b/third_party/txt/src/skia/paragraph_builder_skia.cc @@ -122,6 +122,24 @@ skt::TextStyle TxtToSkia(const TextStyle& txt) { skia.addFontFeature(SkString(ff.first.c_str()), ff.second); } + if (!txt.font_variations.GetAxisValues().empty()) { + std::vector coordinates; + for (const auto& it : txt.font_variations.GetAxisValues()) { + const std::string& axis = it.first; + if (axis.length() != 4) { + continue; + } + coordinates.push_back({ + SkSetFourByteTag(axis[0], axis[1], axis[2], axis[3]), + it.second, + }); + } + SkFontArguments::VariationPosition position = { + coordinates.data(), static_cast(coordinates.size())}; + skia.setFontArguments( + SkFontArguments().setVariationDesignPosition(position)); + } + skia.resetShadows(); for (const txt::TextShadow& txt_shadow : txt.text_shadows) { skt::TextShadow shadow; diff --git a/third_party/txt/src/txt/font_features.cc b/third_party/txt/src/txt/font_features.cc index 0b7b6abdc2b83..fd999ccd98d76 100644 --- a/third_party/txt/src/txt/font_features.cc +++ b/third_party/txt/src/txt/font_features.cc @@ -44,4 +44,12 @@ const std::map& FontFeatures::GetFontFeatures() const { return feature_map_; } +void FontVariations::SetAxisValue(std::string tag, float value) { + axis_map_[tag] = value; +} + +const std::map& FontVariations::GetAxisValues() const { + return axis_map_; +} + } // namespace txt diff --git a/third_party/txt/src/txt/font_features.h b/third_party/txt/src/txt/font_features.h index ca5fe7df41823..b6810ef75cc6d 100644 --- a/third_party/txt/src/txt/font_features.h +++ b/third_party/txt/src/txt/font_features.h @@ -37,6 +37,18 @@ class FontFeatures { std::map feature_map_; }; +// Axis tags and values that can be applied in a text style to control the +// attributes of variable fonts. +class FontVariations { + public: + void SetAxisValue(std::string tag, float value); + + const std::map& GetAxisValues() const; + + private: + std::map axis_map_; +}; + } // namespace txt #endif // LIB_TXT_SRC_FONT_FEATURE_H_ diff --git a/third_party/txt/src/txt/text_style.h b/third_party/txt/src/txt/text_style.h index 99848a812056a..2a63c685c81d2 100644 --- a/third_party/txt/src/txt/text_style.h +++ b/third_party/txt/src/txt/text_style.h @@ -62,6 +62,7 @@ class TextStyle { // the bottom). std::vector text_shadows; FontFeatures font_features; + FontVariations font_variations; TextStyle();