diff --git a/bin/internal/engine.version b/bin/internal/engine.version index e68f757473..c0b49245fe 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -15f2b92cce916982b7dd8ce658bbf2a465c06ba4 +e7eb1c8bf65531195fc76ba96c8fc8478ac5f554 diff --git a/bin/internal/goldens.version b/bin/internal/goldens.version index 76a6e57b95..3c64902222 100644 --- a/bin/internal/goldens.version +++ b/bin/internal/goldens.version @@ -1 +1 @@ -46a3d26acbb1b0d72b6b02c30f03b9dbda7d5bdf +cbd3fa445868962b7e910e498791755c988e9890 diff --git a/packages/flutter/lib/painting.dart b/packages/flutter/lib/painting.dart index 1bd1945633..ef79da4035 100644 --- a/packages/flutter/lib/painting.dart +++ b/packages/flutter/lib/painting.dart @@ -51,6 +51,7 @@ export 'src/painting/paint_utilities.dart'; export 'src/painting/rounded_rectangle_border.dart'; export 'src/painting/shape_decoration.dart'; export 'src/painting/stadium_border.dart'; +export 'src/painting/strut_style.dart'; export 'src/painting/superellipse_shape.dart'; export 'src/painting/text_painter.dart'; export 'src/painting/text_span.dart'; diff --git a/packages/flutter/lib/src/painting/strut_style.dart b/packages/flutter/lib/src/painting/strut_style.dart new file mode 100644 index 0000000000..a6ff94482b --- /dev/null +++ b/packages/flutter/lib/src/painting/strut_style.dart @@ -0,0 +1,478 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +import 'basic_types.dart'; + +/// Defines the strut, which sets the minimum height a line can be +/// relative to the baseline. Strut applies to all lines in the pararaph. +/// +/// Strut is a feature that allows minimum line heights to be set. The effect is as +/// if a zero width space was included at the beginning of each line in the +/// paragraph. This imaginary space is 'shaped' according the properties defined +/// in this class. +/// +/// No lines may be shorter than the strut. The ascent and descent of the strut +/// are calculated, and any laid out text that has a shorter ascent or descent than +/// the strut's ascent or descent will take the ascent and descent of the strut. +/// Text with ascents or descents larger than the strut's ascent or descent will lay +/// out as normal and extend past the strut. +/// +/// Strut is defined independently from any text content or [TextStyle]s. +/// +/// The vertical components of strut are as follows: +/// +/// * `leading * fontSize / 2` or half the font leading if `leading` is undefined (half leading) +/// * `ascent * height` +/// * `descent * height` +/// * `leading * fontSize / 2` or half the font leading if `leading` is undefined (half leading) +/// +/// The sum of these four values is the total height of the line. +/// +/// The `ascent + descent` is equivalent to the [fontSize]. Ascent is the font's +/// spacing above the baseline without leading and descent is the spacing below the +/// baseline without leading. Leading is split evenly between the top and bottom. +/// The values for `ascent` and `descent` are provided by the font named by +/// [fontFamily]. If no [fontFamily] or [fontFamilyFallback] is provided, then the +/// platform's default family will be used. +/// +/// Each line's spacing above the baseline will be at least as tall as the half +/// leading plus ascent. Each line's spacing below the baseline will be at least as +/// tall as the half leading plus descent. +/// +/// ### Fields and their default values. + +// /////////////////////////////////////////////////////////////////////////// +// The defaults are noted here for convenience. The actual place where they // +// are defined is in the engine paragraph_style.h of LibTxt. The values here// +// should be updated should it change in the engine. The engine specifies // +// the defaults in order to reduce the amount of data we pass to native as // +// strut will usually be unspecified. // +// /////////////////////////////////////////////////////////////////////////// + +/// +/// Omitted or null properties will take the default values specified below: +/// +/// * [fontFamily]: the name of the font to use when calcualting the strut (e.g., Roboto). +/// No glyphs from the font will be drawn and the font will be used purely for metrics. +/// +/// * [fontFamilyFallback]: an ordered list of font family names that will be searched for when +/// the font in [fontFamily] cannot be found. When all specified font families have been +/// exhausted an no match was found, the default platform font will be used. +/// +/// * [fontSize]: the size of the ascent plus descent in logical pixels. This is also +/// used as the basis of the custom leading caluclation. This value cannot +/// be negative. +/// Default is 14 logical pixels. +/// +/// * [height]: the multiple of [fontSize] to multiply the ascent and descent by. +/// The [height] will impact the spacing above and below the baseline differently +/// depending on the ratios between the font's ascent and descent. This property is +/// separate from the leading multiplier, which is controlled through [leading]. +/// Default is 1.0. +/// +/// * [leading]: the custom leading to apply to the strut as a multiple of [fontSize]. +/// Leading is additional spacing between lines. Half of the leading is added +/// to the top and the other half to the bottom of the line height. This differs +/// from [height] since the spacing is equally distributed above and below the +/// baseline. +/// Default is `null`, which will use the font-specified leading. +/// +/// * [fontWeight]: the typeface thickness to use when calculating the strut (e.g., bold). +/// Default is [FontWeight.w400]. +/// +/// * [fontStyle]: the typeface variant to use when calculating the strut (e.g., italic). +/// Default is [FontStyle.normal]. +/// +/// * [forceStrutHeight]: when true, all lines will be laid out with the height of the +/// strut. All line and run-specific metrics will be ignored/overridden and only strut +/// metrics will be used instead. This property guarantees uniform line spacing, however +/// text in adjacent lines may overlap. This property should be enabled with caution as +/// it bypasses a large portion of the vertical layout system. +/// The default value is false. +/// +/// ### Examples +/// +/// {@tool sample} +/// In this simple case, the text will be rendered at font size 10, however, the vertical +/// height of each line will be the strut height (Roboto in font size 30 * 1.5) as the text +/// itself is shorter than the strut. +/// +/// ```dart +/// const Text( +/// 'Hello, world!\nSecond line!', +/// style: TextStyle( +/// fontSize: 10, +/// fontFamily: 'Raleway', +/// ), +/// strutStyle: StrutStyle( +/// fontFamily: 'Roboto', +/// fontSize: 30, +/// height: 1.5, +/// ), +/// ), +/// ``` +/// {@end-tool} +/// +/// {@tool sample} +/// Here, strut is used to absorb the additional line height in the second line. +/// The strut [height] was defined as 1.5 (the default font size is 14), which +/// caused all lines to be laid out taller than without strut. This extra space was +/// able to accomodate the larger font size of `Second line!` without causing the line +/// height to change for the second line only. All lines in this example are thus the +/// same height (`14 * 1.5`). +/// +/// ```dart +/// const Text.rich( +/// TextSpan( +/// text: 'First line!\n', +/// style: TextStyle( +/// fontSize: 14, +/// fontFamily: 'Roboto' +/// ), +/// children: [ +/// TextSpan( +/// text: 'Second line!\n', +/// style: TextStyle( +/// fontSize: 16, +/// fontFamily: 'Roboto', +/// ), +/// ), +/// TextSpan( +/// text: 'Third line!\n', +/// style: TextStyle( +/// fontSize: 14, +/// fontFamily: 'Roboto', +/// ), +/// ), +/// ], +/// ), +/// strutStyle: StrutStyle( +/// fontFamily: 'Roboto', +/// height: 1.5, +/// ), +/// ), +/// ``` +/// {@end-tool} +/// +/// {@tool sample} +/// Here, strut is used to enable strange and overlapping text to achieve unique +/// effects. The `M`s in lines 2 and 3 are able to extend above their lines and +/// fill empty space in lines above. The [forceStrutHeight] is enabled and functions +/// as a 'grid' for the glyphs to draw on. +/// +/// ![The result of the example below.](https://flutter.github.io/assets-for-api-docs/assets/painting/strut_force_example.png) +/// +/// ```dart +/// const Text.rich( +/// TextSpan( +/// text: '--------- ---------\n', +/// style: TextStyle( +/// fontSize: 14, +/// fontFamily: 'Roboto', +/// ), +/// children: [ +/// TextSpan( +/// text: '^^^M^^^\n', +/// style: TextStyle( +/// fontSize: 30, +/// fontFamily: 'Roboto', +/// ), +/// ), +/// TextSpan( +/// text: 'M------M\n', +/// style: TextStyle( +/// fontSize: 30, +/// fontFamily: 'Roboto', +/// ), +/// ), +/// ], +/// ), +/// strutStyle: StrutStyle( +/// fontFamily: 'Roboto', +/// fontSize: 14, +/// height: 1, +/// forceStrutHeight: true, +/// ), +/// ), +/// ``` +/// {@end-tool} +/// +/// {@tool sample} +/// This example uses forceStrutHeight to create a 'drop cap' for the 'T' in 'The'. +/// By locking the line heights to the metrics of the 14pt serif font, we are able +/// to lay out a large 37pt 'T' on the second line to take up space on both the first +/// and second lines. +/// +/// ![The result of the example below.](https://flutter.github.io/assets-for-api-docs/assets/painting/strut_force_example_2.png) +/// +/// ```dart +/// Text.rich( +/// TextSpan( +/// text: '  he candle flickered\n', +/// style: TextStyle( +/// fontSize: 14, +/// fontFamily: 'Serif' +/// ), +/// children: [ +/// TextSpan( +/// text: 'T', +/// style: TextStyle( +/// fontSize: 37, +/// fontFamily: 'Serif' +/// ), +/// ), +/// TextSpan( +/// text: 'in the moonlight as\n', +/// style: TextStyle( +/// fontSize: 14, +/// fontFamily: 'Serif' +/// ), +/// ), +/// TextSpan( +/// text: 'Dash the bird fluttered\n', +/// style: TextStyle( +/// fontSize: 14, +/// fontFamily: 'Serif' +/// ), +/// ), +/// TextSpan( +/// text: 'off into the distance.', +/// style: TextStyle( +/// fontSize: 14, +/// fontFamily: 'Serif' +/// ), +/// ), +/// ], +/// ), +/// strutStyle: StrutStyle( +/// fontFamily: 'Serif', +/// fontSize: 14, +/// forceStrutHeight: true, +/// ), +/// ), +/// ``` +/// {@end-tool} +/// +@immutable +class StrutStyle extends Diagnosticable { + /// Creates a strut style. + /// + /// The `package` argument must be non-null if the font family is defined in a + /// package. It is combined with the `fontFamily` argument to set the + /// [fontFamily] property. + const StrutStyle({ + String fontFamily, + List fontFamilyFallback, + this.fontSize, + this.height, + this.leading, + this.fontWeight, + this.fontStyle, + this.forceStrutHeight, + this.debugLabel, + String package, + }) : fontFamily = package == null ? fontFamily : 'packages/$package/$fontFamily', + _fontFamilyFallback = fontFamilyFallback, + _package = package, + assert(fontSize == null || fontSize > 0), + assert(leading == null || leading >= 0), + assert(package == null || (package != null && (fontFamily != null || fontFamilyFallback != null))); + + /// The name of the font to use when calcualting the strut (e.g., Roboto). If the + /// font is defined in a package, this will be prefixed with + /// 'packages/package_name/' (e.g. 'packages/cool_fonts/Roboto'). The + /// prefixing is done by the constructor when the `package` argument is + /// provided. + /// + /// The value provided in [fontFamily] will act as the preferred/first font + /// family that will be searched for, followed in order by the font families + /// in [fontFamilyFallback]. If all font families are exhausted and no match + /// was found, the default platform font family will be used instead. Unlike + /// [TextStyle.fontFamilyFallback], the font does not need to contain the + /// desired glyphs to match. + final String fontFamily; + + /// The ordered list of font families to fall back on when a higher priority + /// font family cannot be found. + /// + /// The value provided in [fontFamily] will act as the preferred/first font + /// family that will be searched for, followed in order by the font families + /// in [fontFamilyFallback]. If all font families are exhausted and no match + /// was found, the default platform font family will be used instead. Unlike + /// [TextStyle.fontFamilyFallback], the font does not need to contain the + /// desired glyphs to match. + /// + /// When [fontFamily] is null or not provided, the first value in [fontFamilyFallback] + /// acts as the preferred/first font family. When neither is provided, then + /// the default platform font will be used. Providing and empty list or null + /// for this property is the same as omitting it. + /// + /// If the font is defined in a package, each font family in the list will be + /// prefixed with 'packages/package_name/' (e.g. 'packages/cool_fonts/Roboto'). + /// The package name should be provided by the `package` argument in the + /// constructor. + List get fontFamilyFallback { + if (_package != null && _fontFamilyFallback != null) + return _fontFamilyFallback.map((String family) => 'packages/$_package/$family').toList(); + return _fontFamilyFallback; + } + final List _fontFamilyFallback; + + // This is stored in order to prefix the fontFamilies in _fontFamilyFallback + // in the [fontFamilyFallback] getter. + final String _package; + + /// The size of text (in logical pixels) to use when obtaining metrics from the font. + /// + /// The [fontSize] is used to get the base set of metrics that are then used to calculated + /// the metrics of strut. The height and leading are expressed as a multiple of + /// [fontSize]. + /// + /// The default fontSize is 14 logical pixels. + final double fontSize; + + /// The multiple of [fontSize] to multiply the ascent and descent by where `ascent + descent = fontSize`. + /// + /// Ascent is the spacing above the baseline and descent is the spacing below the baseline. + /// + /// The [height] will impact the spacing above and below the baseline differently + /// depending on the ratios between the font's ascent and descent. This property is + /// separate from the leading multiplier, which is controlled through [leading]. + /// + /// The default height is 1.0. + final double height; + + /// The typeface thickness to use when calculating the strut (e.g., bold). + /// + /// The default fontWeight is [FontWeight.w400]. + final FontWeight fontWeight; + + /// The typeface variant to use when calculating the strut (e.g., italics). + /// + /// The default fontStyle is [FontStyle.normal]. + final FontStyle fontStyle; + + /// The custom leading to apply to the strut as a multiple of [fontSize]. + /// + /// Leading is additional spacing between lines. Half of the leading is added + /// to the top and the other half to the bottom of the line. This differs + /// from [height] since the spacing is equally distributed above and below the + /// baseline. + /// + /// The default leading is null, which will use the font-specified leading. + final double leading; + + /// Whether the strut height should be forced. + /// + /// When true, all lines will be laid out with the height of the + /// strut. All line and run-specific metrics will be ignored/overridden and only strut + /// metrics will be used instead. This property guarantees uniform line spacing, however + /// text in adjacent lines may overlap. + /// + /// This property should be enabled with caution as + /// it bypasses a large portion of the vertical layout system. + /// + /// This is equivalent to setting [TextStyle.height] to zero for all [TextStyle]s + /// in the paragraph. Since the height of each line is calculated as a max of the + /// metrics of each run of text, zero height [TextStyle]s cause the minimums + /// defined by strut to always manifest, resulting in all lines having the height + /// of the strut. + /// + /// The default is false. + final bool forceStrutHeight; + + /// A human-readable description of this strut style. + /// + /// This property is maintained only in debug builds. + /// + /// This property is not considered when comparing strut styles using `==` or + /// [compareTo], and it does not affect [hashCode]. + final String debugLabel; + + /// Describe the difference between this style and another, in terms of how + /// much damage it will make to the rendering. + /// + /// See also: + /// + /// * [TextSpan.compareTo], which does the same thing for entire [TextSpan]s. + RenderComparison compareTo(StrutStyle other) { + if (identical(this, other)) + return RenderComparison.identical; + if (fontFamily != other.fontFamily || + fontSize != other.fontSize || + fontWeight != other.fontWeight || + fontStyle != other.fontStyle || + height != other.height || + leading != other.leading || + forceStrutHeight != other.forceStrutHeight || + !listEquals(fontFamilyFallback, other.fontFamilyFallback)) + return RenderComparison.layout; + return RenderComparison.identical; + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + final StrutStyle typedOther = other; + return fontFamily == typedOther.fontFamily && + fontSize == typedOther.fontSize && + fontWeight == typedOther.fontWeight && + fontStyle == typedOther.fontStyle && + height == typedOther.height && + leading == typedOther.leading && + forceStrutHeight == typedOther.forceStrutHeight; + } + + @override + int get hashCode { + return hashValues( + fontFamily, + fontSize, + fontWeight, + fontStyle, + height, + leading, + forceStrutHeight, + ); + } + + /// Adds all properties prefixing property names with the optional `prefix`. + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties, { String prefix = '' }) { + super.debugFillProperties(properties); + if (debugLabel != null) + properties.add(MessageProperty('${prefix}debugLabel', debugLabel)); + final List styles = []; + styles.add(StringProperty('${prefix}family', fontFamily, defaultValue: null, quoted: false)); + styles.add(IterableProperty('${prefix}familyFallback', fontFamilyFallback, defaultValue: null)); + styles.add(DoubleProperty('${prefix}size', fontSize, defaultValue: null)); + String weightDescription; + if (fontWeight != null) { + weightDescription = '${fontWeight.index + 1}00'; + } + // TODO(jacobr): switch this to use enumProperty which will either cause the + // weight description to change to w600 from 600 or require existing + // enumProperty to handle this special case. + styles.add(DiagnosticsProperty( + '${prefix}weight', + fontWeight, + description: weightDescription, + defaultValue: null, + )); + styles.add(EnumProperty('${prefix}style', fontStyle, defaultValue: null)); + styles.add(DoubleProperty('${prefix}height', height, unit: 'x', defaultValue: null)); + styles.add(FlagProperty('${prefix}forceStrutHeight', value: forceStrutHeight, defaultValue: null)); + + final bool styleSpecified = styles.any((DiagnosticsNode n) => !n.isFiltered(DiagnosticLevel.info)); + styles.forEach(properties.add); + + if (!styleSpecified) + properties.add(FlagProperty('forceStrutHeight', value: forceStrutHeight, ifTrue: '$prefix', ifFalse: '$prefix')); + } +} diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart index 1d38ef9868..0d8da07288 100644 --- a/packages/flutter/lib/src/painting/text_painter.dart +++ b/packages/flutter/lib/src/painting/text_painter.dart @@ -10,6 +10,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'basic_types.dart'; +import 'strut_style.dart'; import 'text_span.dart'; export 'package:flutter/services.dart' show TextRange, TextSelection; @@ -48,6 +49,7 @@ class TextPainter { int maxLines, String ellipsis, Locale locale, + StrutStyle strutStyle, }) : assert(text == null || text.debugAssertIsValid()), assert(textAlign != null), assert(textScaleFactor != null), @@ -58,7 +60,8 @@ class TextPainter { _textScaleFactor = textScaleFactor, _maxLines = maxLines, _ellipsis = ellipsis, - _locale = locale; + _locale = locale, + _strutStyle = strutStyle; ui.Paragraph _paragraph; bool _needsLayout = true; @@ -198,6 +201,29 @@ class TextPainter { _needsLayout = true; } + /// {@template flutter.painting.textPainter.strutStyle} + /// The strut style to use. Strut style defines the strut, which sets minimum + /// vertical layout metrics. + /// + /// Omitting or providing null will disable strut. + /// + /// Omitting or providing null for any properties of [StrutStyle] will result in + /// default values being used. It is highly recommended to at least specify a + /// [fontSize]. + /// + /// See [StrutStyle] for details. + /// {@endtemplate} + StrutStyle get strutStyle => _strutStyle; + StrutStyle _strutStyle; + set strutStyle(StrutStyle value) { + if (_strutStyle == value) + return; + _strutStyle = value; + _paragraph = null; + _needsLayout = true; + } + + ui.Paragraph _layoutTemplate; ui.ParagraphStyle _createParagraphStyle([TextDirection defaultTextDirection]) { @@ -212,6 +238,7 @@ class TextPainter { maxLines: _maxLines, ellipsis: _ellipsis, locale: _locale, + strutStyle: _strutStyle, ) ?? ui.ParagraphStyle( textAlign: textAlign, textDirection: textDirection ?? defaultTextDirection, diff --git a/packages/flutter/lib/src/painting/text_style.dart b/packages/flutter/lib/src/painting/text_style.dart index 821cb475bf..7cfc700939 100644 --- a/packages/flutter/lib/src/painting/text_style.dart +++ b/packages/flutter/lib/src/painting/text_style.dart @@ -2,11 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' as ui show ParagraphStyle, TextStyle, lerpDouble, Shadow; +import 'dart:ui' as ui show ParagraphStyle, TextStyle, StrutStyle, lerpDouble, Shadow; import 'package:flutter/foundation.dart'; import 'basic_types.dart'; +import 'strut_style.dart'; const String _kDefaultDebugLabel = 'unknown'; @@ -812,17 +813,35 @@ class TextStyle extends Diagnosticable { String ellipsis, int maxLines, Locale locale, + String fontFamily, + double fontSize, + FontWeight fontWeight, + FontStyle fontStyle, + double height, + StrutStyle strutStyle, }) { assert(textScaleFactor != null); assert(maxLines == null || maxLines > 0); return ui.ParagraphStyle( textAlign: textAlign, textDirection: textDirection, - fontWeight: fontWeight, - fontStyle: fontStyle, - fontFamily: fontFamily, - fontSize: (fontSize ?? _defaultFontSize) * textScaleFactor, - lineHeight: height, + // Here, we stablish the contents of this TextStyle as the paragraph's default font + // unless an override is passed in. + fontWeight: fontWeight ?? this.fontWeight, + fontStyle: fontStyle ?? this.fontStyle, + fontFamily: fontFamily ?? this.fontFamily, + fontSize: (fontSize ?? this.fontSize ?? _defaultFontSize) * textScaleFactor, + height: height ?? this.height, + strutStyle: strutStyle == null ? null : ui.StrutStyle( + fontFamily: strutStyle.fontFamily, + fontFamilyFallback: strutStyle.fontFamilyFallback, + fontSize: strutStyle.fontSize, + height: strutStyle.height, + leading: strutStyle.leading, + fontWeight: strutStyle.fontWeight, + fontStyle: strutStyle.fontStyle, + forceStrutHeight: strutStyle.forceStrutHeight, + ), maxLines: maxLines, ellipsis: ellipsis, locale: locale, @@ -928,35 +947,7 @@ class TextStyle extends Diagnosticable { styles.add(DoubleProperty('${prefix}size', fontSize, defaultValue: null)); String weightDescription; if (fontWeight != null) { - switch (fontWeight) { - case FontWeight.w100: - weightDescription = '100'; - break; - case FontWeight.w200: - weightDescription = '200'; - break; - case FontWeight.w300: - weightDescription = '300'; - break; - case FontWeight.w400: - weightDescription = '400'; - break; - case FontWeight.w500: - weightDescription = '500'; - break; - case FontWeight.w600: - weightDescription = '600'; - break; - case FontWeight.w700: - weightDescription = '700'; - break; - case FontWeight.w800: - weightDescription = '800'; - break; - case FontWeight.w900: - weightDescription = '900'; - break; - } + weightDescription = '${fontWeight.index + 1}00'; } // TODO(jacobr): switch this to use enumProperty which will either cause the // weight description to change to w600 from 600 or require existing diff --git a/packages/flutter/lib/src/rendering/error.dart b/packages/flutter/lib/src/rendering/error.dart index 03ab647493..a8552b37b5 100644 --- a/packages/flutter/lib/src/rendering/error.dart +++ b/packages/flutter/lib/src/rendering/error.dart @@ -95,7 +95,7 @@ class RenderErrorBox extends RenderBox { /// The paragraph style to use when painting [RenderErrorBox] objects. static ui.ParagraphStyle paragraphStyle = ui.ParagraphStyle( - lineHeight: 1.0, + height: 1.0, ); @override diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index a1dfe04aaf..1742c190fa 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -6,6 +6,7 @@ import 'dart:ui' as ui show Gradient, Shader, TextBox; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; +import 'package:flutter/painting.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter/services.dart'; @@ -45,6 +46,7 @@ class RenderParagraph extends RenderBox { double textScaleFactor = 1.0, int maxLines, Locale locale, + StrutStyle strutStyle, }) : assert(text != null), assert(text.debugAssertIsValid()), assert(textAlign != null), @@ -63,6 +65,7 @@ class RenderParagraph extends RenderBox { maxLines: maxLines, ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null, locale: locale, + strutStyle: strutStyle, ); final TextPainter _textPainter; @@ -194,6 +197,17 @@ class RenderParagraph extends RenderBox { markNeedsLayout(); } + /// {@macro flutter.painting.textPainter.strutStyle} + StrutStyle get strutStyle => _textPainter.strutStyle; + /// The value may be null. + set strutStyle(StrutStyle value) { + if (_textPainter.strutStyle == value) + return; + _textPainter.strutStyle = value; + _overflowShader = null; + markNeedsLayout(); + } + void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) { final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis; _textPainter.layout(minWidth: minWidth, maxWidth: widthMatters ? maxWidth : double.infinity); diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index be070c47ab..3da10ebfe6 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -4599,6 +4599,7 @@ class RichText extends LeafRenderObjectWidget { this.textScaleFactor = 1.0, this.maxLines, this.locale, + this.strutStyle, }) : assert(text != null), assert(textAlign != null), assert(softWrap != null), @@ -4660,6 +4661,9 @@ class RichText extends LeafRenderObjectWidget { /// See [RenderParagraph.locale] for more information. final Locale locale; + /// {@macro flutter.painting.textPainter.strutStyle} + final StrutStyle strutStyle; + @override RenderParagraph createRenderObject(BuildContext context) { assert(textDirection != null || debugCheckHasDirectionality(context)); @@ -4670,6 +4674,7 @@ class RichText extends LeafRenderObjectWidget { overflow: overflow, textScaleFactor: textScaleFactor, maxLines: maxLines, + strutStyle: strutStyle, locale: locale ?? Localizations.localeOf(context, nullOk: true), ); } @@ -4685,6 +4690,7 @@ class RichText extends LeafRenderObjectWidget { ..overflow = overflow ..textScaleFactor = textScaleFactor ..maxLines = maxLines + ..strutStyle = strutStyle ..locale = locale ?? Localizations.localeOf(context, nullOk: true); } diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart index d85b6700ef..7e851a7279 100644 --- a/packages/flutter/lib/src/widgets/text.dart +++ b/packages/flutter/lib/src/widgets/text.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; import 'basic.dart'; import 'framework.dart'; @@ -224,6 +225,7 @@ class Text extends StatelessWidget { const Text(this.data, { Key key, this.style, + this.strutStyle, this.textAlign, this.textDirection, this.locale, @@ -240,6 +242,7 @@ class Text extends StatelessWidget { const Text.rich(this.textSpan, { Key key, this.style, + this.strutStyle, this.textAlign, this.textDirection, this.locale, @@ -269,6 +272,9 @@ class Text extends StatelessWidget { /// replace the closest enclosing [DefaultTextStyle]. final TextStyle style; + /// {@macro flutter.painting.textPainter.strutStyle} + final StrutStyle strutStyle; + /// How the text should be aligned horizontally. final TextAlign textAlign; @@ -356,6 +362,7 @@ class Text extends StatelessWidget { overflow: overflow ?? defaultTextStyle.overflow, textScaleFactor: textScaleFactor ?? MediaQuery.textScaleFactorOf(context), maxLines: maxLines ?? defaultTextStyle.maxLines, + strutStyle: strutStyle, text: TextSpan( style: effectiveTextStyle, text: data, diff --git a/packages/flutter/test/material/theme_test.dart b/packages/flutter/test/material/theme_test.dart index 474a0322eb..7d58fab468 100644 --- a/packages/flutter/test/material/theme_test.dart +++ b/packages/flutter/test/material/theme_test.dart @@ -720,7 +720,7 @@ class _TextStyleProxy implements TextStyle { } @override - ui.ParagraphStyle getParagraphStyle({TextAlign textAlign, TextDirection textDirection, double textScaleFactor = 1.0, String ellipsis, int maxLines, Locale locale}) { + ui.ParagraphStyle getParagraphStyle({TextAlign textAlign, TextDirection textDirection, double textScaleFactor = 1.0, String ellipsis, int maxLines, Locale locale, String fontFamily, double fontSize, FontWeight fontWeight, FontStyle fontStyle, double height, StrutStyle strutStyle}) { throw UnimplementedError(); } diff --git a/packages/flutter/test/painting/text_style_test.dart b/packages/flutter/test/painting/text_style_test.dart index 4d65194e99..f26570aec3 100644 --- a/packages/flutter/test/painting/text_style_test.dart +++ b/packages/flutter/test/painting/text_style_test.dart @@ -169,22 +169,22 @@ void main() { expect(ts2.toString(), 'TextStyle(color: Color(0xff00ff00), decoration: unspecified, decorationColor: unspecified, decorationStyle: unspecified, fontWeight: FontWeight.w800, fontStyle: unspecified, textBaseline: unspecified, fontFamily: unspecified, fontFamilyFallback: unspecified, fontSize: 10.0, letterSpacing: unspecified, wordSpacing: unspecified, height: 100.0x, locale: unspecified, background: unspecified, foreground: unspecified, shadows: unspecified)'); final ui.ParagraphStyle ps2 = s2.getParagraphStyle(textAlign: TextAlign.center); - expect(ps2, equals(ui.ParagraphStyle(textAlign: TextAlign.center, fontWeight: FontWeight.w800, fontSize: 10.0, lineHeight: 100.0))); - expect(ps2.toString(), 'ParagraphStyle(textAlign: TextAlign.center, textDirection: unspecified, fontWeight: FontWeight.w800, fontStyle: unspecified, maxLines: unspecified, fontFamily: unspecified, fontSize: 10.0, lineHeight: 100.0x, ellipsis: unspecified, locale: unspecified)'); + expect(ps2, equals(ui.ParagraphStyle(textAlign: TextAlign.center, fontWeight: FontWeight.w800, fontSize: 10.0, height: 100.0))); + expect(ps2.toString(), 'ParagraphStyle(textAlign: TextAlign.center, textDirection: unspecified, fontWeight: FontWeight.w800, fontStyle: unspecified, maxLines: unspecified, fontFamily: unspecified, fontSize: 10.0, height: 100.0x, ellipsis: unspecified, locale: unspecified)'); final ui.ParagraphStyle ps5 = s5.getParagraphStyle(); - expect(ps5, equals(ui.ParagraphStyle(fontWeight: FontWeight.w700, fontSize: 12.0, lineHeight: 123.0))); - expect(ps5.toString(), 'ParagraphStyle(textAlign: unspecified, textDirection: unspecified, fontWeight: FontWeight.w700, fontStyle: unspecified, maxLines: unspecified, fontFamily: unspecified, fontSize: 12.0, lineHeight: 123.0x, ellipsis: unspecified, locale: unspecified)'); + expect(ps5, equals(ui.ParagraphStyle(fontWeight: FontWeight.w700, fontSize: 12.0, height: 123.0))); + expect(ps5.toString(), 'ParagraphStyle(textAlign: unspecified, textDirection: unspecified, fontWeight: FontWeight.w700, fontStyle: unspecified, maxLines: unspecified, fontFamily: unspecified, fontSize: 12.0, height: 123.0x, ellipsis: unspecified, locale: unspecified)'); }); test('TextStyle with text direction', () { final ui.ParagraphStyle ps6 = const TextStyle().getParagraphStyle(textDirection: TextDirection.ltr); expect(ps6, equals(ui.ParagraphStyle(textDirection: TextDirection.ltr, fontSize: 14.0))); - expect(ps6.toString(), 'ParagraphStyle(textAlign: unspecified, textDirection: TextDirection.ltr, fontWeight: unspecified, fontStyle: unspecified, maxLines: unspecified, fontFamily: unspecified, fontSize: 14.0, lineHeight: unspecified, ellipsis: unspecified, locale: unspecified)'); + expect(ps6.toString(), 'ParagraphStyle(textAlign: unspecified, textDirection: TextDirection.ltr, fontWeight: unspecified, fontStyle: unspecified, maxLines: unspecified, fontFamily: unspecified, fontSize: 14.0, height: unspecified, ellipsis: unspecified, locale: unspecified)'); final ui.ParagraphStyle ps7 = const TextStyle().getParagraphStyle(textDirection: TextDirection.rtl); expect(ps7, equals(ui.ParagraphStyle(textDirection: TextDirection.rtl, fontSize: 14.0))); - expect(ps7.toString(), 'ParagraphStyle(textAlign: unspecified, textDirection: TextDirection.rtl, fontWeight: unspecified, fontStyle: unspecified, maxLines: unspecified, fontFamily: unspecified, fontSize: 14.0, lineHeight: unspecified, ellipsis: unspecified, locale: unspecified)'); + expect(ps7.toString(), 'ParagraphStyle(textAlign: unspecified, textDirection: TextDirection.rtl, fontWeight: unspecified, fontStyle: unspecified, maxLines: unspecified, fontFamily: unspecified, fontSize: 14.0, height: unspecified, ellipsis: unspecified, locale: unspecified)'); }); test('TextStyle using package font', () { diff --git a/packages/flutter/test/widgets/text_golden_test.dart b/packages/flutter/test/widgets/text_golden_test.dart index b90d7ffa7a..4adb609685 100644 --- a/packages/flutter/test/widgets/text_golden_test.dart +++ b/packages/flutter/test/widgets/text_golden_test.dart @@ -220,4 +220,221 @@ void main() { matchesGoldenFile('text_golden.Fade.1.png'), ); }, skip: !Platform.isLinux); + + testWidgets('Default Strut text', (WidgetTester tester) async { + await tester.pumpWidget( + Center( + child: RepaintBoundary( + child: Container( + width: 200.0, + height: 100.0, + decoration: const BoxDecoration( + color: Color(0xff00ff00), + ), + child: const Text('Hello\nLine 2\nLine 3', + textDirection: TextDirection.ltr, + style: TextStyle(), + strutStyle: StrutStyle(), + ), + ), + ), + ), + ); + await expectLater( + find.byType(Container), + matchesGoldenFile('text_golden.StrutDefault.png'), + ); + }, skip: !Platform.isLinux); + + testWidgets('Strut text 1', (WidgetTester tester) async { + await tester.pumpWidget( + Center( + child: RepaintBoundary( + child: Container( + width: 200.0, + height: 100.0, + decoration: const BoxDecoration( + color: Color(0xff00ff00), + ), + child: const Text('Hello\nLine2\nLine3', + textDirection: TextDirection.ltr, + style: TextStyle(), + strutStyle: StrutStyle( + height: 1.5, + ), + ), + ), + ), + ), + ); + await expectLater( + find.byType(Container), + matchesGoldenFile('text_golden.Strut.1.png'), + ); + }, skip: !Platform.isLinux); + + testWidgets('Strut text 2', (WidgetTester tester) async { + await tester.pumpWidget( + Center( + child: RepaintBoundary( + child: Container( + width: 200.0, + height: 100.0, + decoration: const BoxDecoration( + color: Color(0xff00ff00), + ), + child: const Text('Hello\nLine 2\nLine 3', + textDirection: TextDirection.ltr, + style: TextStyle(), + strutStyle: StrutStyle( + height: 1.5, + fontSize: 14, + ), + ), + ), + ), + ), + ); + await expectLater( + find.byType(Container), + matchesGoldenFile('text_golden.Strut.2.png'), + ); + }, skip: !Platform.isLinux); + + testWidgets('Strut text rich', (WidgetTester tester) async { + await tester.pumpWidget( + Center( + child: RepaintBoundary( + child: Container( + width: 200.0, + height: 150.0, + decoration: const BoxDecoration( + color: Color(0xff00ff00), + ), + child: const Text.rich( + TextSpan( + text: 'Hello\n', + style: TextStyle( + color: Colors.red, + fontSize: 30 + ), + children: [ + TextSpan( + text: 'Second line!\n', + style: TextStyle( + fontSize: 5, + color: Colors.blue, + ), + ), + TextSpan( + text: 'Third line!\n', + style: TextStyle( + fontSize: 25, + color: Colors.white, + ), + ), + ], + ), + textDirection: TextDirection.ltr, + strutStyle: StrutStyle( + fontSize: 14, + height: 1.1, + leading: 0.1, + ), + ), + ), + ), + ), + ); + await expectLater( + find.byType(Container), + matchesGoldenFile('text_golden.Strut.3.png'), + ); + }, skip: !Platform.isLinux); + + testWidgets('Strut text font fallback', (WidgetTester tester) async { + // Font Fallback + await tester.pumpWidget( + Center( + child: RepaintBoundary( + child: Container( + width: 200.0, + height: 100.0, + decoration: const BoxDecoration( + color: Color(0xff00ff00), + ), + child: const Text('Hello\nLine 2\nLine 3', + textDirection: TextDirection.ltr, + style: TextStyle(), + strutStyle: StrutStyle( + fontFamily: 'FakeFont 1', + fontFamilyFallback: [ + 'FakeFont 2', + 'EvilFont 3', + 'Nice Font 4', + 'ahem' + ], + fontSize: 14, + ), + ), + ), + ), + ), + ); + await expectLater( + find.byType(Container), + matchesGoldenFile('text_golden.Strut.4.png'), + ); + }, skip: !Platform.isLinux); + + testWidgets('Strut text rich forceStrutHeight', (WidgetTester tester) async { + await tester.pumpWidget( + Center( + child: RepaintBoundary( + child: Container( + width: 200.0, + height: 100.0, + decoration: const BoxDecoration( + color: Color(0xff00ff00), + ), + child: const Text.rich( + TextSpan( + text: 'Hello\n', + style: TextStyle( + color: Colors.red, + fontSize: 30 + ), + children: [ + TextSpan( + text: 'Second line!\n', + style: TextStyle( + fontSize: 9, + color: Colors.blue, + ), + ), + TextSpan( + text: 'Third line!\n', + style: TextStyle( + fontSize: 27, + color: Colors.white, + ), + ), + ], + ), + textDirection: TextDirection.ltr, + strutStyle: StrutStyle( + fontSize: 14, + height: 1.1, + forceStrutHeight: true, + ), + ), + ), + ), + ), + ); + await expectLater( + find.byType(Container), + matchesGoldenFile('text_golden.StrutForce.1.png'), + ); + }, skip: !Platform.isLinux); }