// Copyright 2014 The Flutter 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'; import 'text_style.dart'; /// Defines the strut, which sets the minimum height a line can be /// relative to the baseline. /// /// Strut applies to all lines in the paragraph. 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. Flutter's strut is based on /// [typesetting strut](https://en.wikipedia.org/wiki/Strut_(typesetting)) /// and CSS's [line-height](https://www.w3.org/TR/CSS2/visudet.html#line-height). /// /// 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 layout 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: /// /// * Half the font-defined leading /// * `ascent * height` /// * `descent * height` /// * Half the font-defined leading /// /// The sum of these four values is the total height of the line. /// /// 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. Many fonts will have leading values of /// zero, so in practice, the leading component is often irrelevant. /// /// When [height] is omitted or null, then the font defined ascent and descent /// will be used. The font's combined ascent and descent may be taller or /// shorter than the [fontSize]. When [height] is provided, the line's EM-square /// ascent and descent (which sums to [fontSize]) will be scaled by [height] to /// achieve a final line height of `fontSize * height + fontSize * leading` /// logical pixels. The proportion of ascent:descent with [height] specified /// is the same as the font metrics defined ascent:descent ratio. /// /// ![Text height diagram](https://flutter.github.io/assets-for-api-docs/assets/painting/text_height_diagram.png) /// /// 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. /// /// See also: /// /// * [StrutStyle](dart-ui/StrutStyle-class.html), the class in the [dart:ui] library. /// /// ### 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 calculating 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 calculation. This value /// cannot be negative. /// Default is 14 logical pixels. /// /// * [height]: the multiple of [fontSize] the line's height should be. /// The line's height will take the font's ascent and descent values if /// [height] is omitted or null. If provided, the EM-square ascent and /// descent (which sum to [fontSize]) is scaled by [height]. /// 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 null. /// /// * [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 snippet} /// 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 snippet} /// 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 accommodate 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>[ /// 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 snippet} /// 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>[ /// 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 snippet} /// 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>[ /// 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 with 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. /// /// If provided, fontSize must be positive and non-zero, leading must be /// zero or positive. const StrutStyle({ String? fontFamily, List<String>? 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 || (fontFamily != null || fontFamilyFallback != null)); /// Builds a StrutStyle that contains values of the equivalent properties in /// the provided [textStyle]. /// /// The [textStyle] parameter must not be null. /// /// The named parameters override the [textStyle]'s argument's properties. /// Since TextStyle does not contain [leading] or [forceStrutHeight], these /// values will take on default values (null and false) unless otherwise /// specified. /// /// If provided, fontSize must be positive and non-zero, leading must be /// zero or positive. /// /// When [textStyle] has a package and a new [package] is also specified, /// the entire font family fallback list should be redefined since the /// [textStyle]'s package data is inherited by being prepended onto the /// font family names. If [fontFamilyFallback] is meant to be empty, pass /// an empty list instead of null. This prevents the previous package name /// from being prepended twice. StrutStyle.fromTextStyle( TextStyle textStyle, { String? fontFamily, List<String>? fontFamilyFallback, double? fontSize, double? height, this.leading, // TextStyle does not have an equivalent (yet). FontWeight? fontWeight, FontStyle? fontStyle, this.forceStrutHeight, String? debugLabel, String? package, }) : assert(textStyle != null), assert(fontSize == null || fontSize > 0), assert(leading == null || leading >= 0), assert(package == null || fontFamily != null || fontFamilyFallback != null), fontFamily = fontFamily != null ? (package == null ? fontFamily : 'packages/$package/$fontFamily') : textStyle.fontFamily, _fontFamilyFallback = fontFamilyFallback ?? textStyle.fontFamilyFallback, height = height ?? textStyle.height, fontSize = fontSize ?? textStyle.fontSize, fontWeight = fontWeight ?? textStyle.fontWeight, fontStyle = fontStyle ?? textStyle.fontStyle, debugLabel = debugLabel ?? textStyle.debugLabel, _package = package; // the textStyle._package data is embedded in the // fontFamily names, so we no longer need it. /// A [StrutStyle] that will have no impact on the text layout. /// /// Equivalent to having no strut at all. All lines will be laid out according to /// the properties defined in [TextStyle]. static const StrutStyle disabled = StrutStyle( height: 0.0, leading: 0.0, ); /// The name of the font to use when calculating 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<String>? get fontFamilyFallback { if (_package != null && _fontFamilyFallback != null) return _fontFamilyFallback!.map((String family) => 'packages/$_package/$family').toList(); return _fontFamilyFallback; } final List<String>? _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. /// /// When [height] is omitted or null, then the font defined ascent and descent /// will be used. The font's combined ascent and descent may be taller or /// shorter than the [fontSize]. When [height] is provided, the line's EM-square /// ascent and descent (which sums to [fontSize]) will be scaled by [height] to /// achieve a final line height of `fontSize * height + fontSize * leading` /// logical pixels. The following diagram illustrates the differences between /// the font metrics defined height and the EM-square height: /// /// ![Text height diagram](https://flutter.github.io/assets-for-api-docs/assets/painting/text_height_diagram.png) /// /// 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 ratio of ascent:descent with [height] specified is the same as the /// font metrics defined ascent:descent ratio when [height] is null or omitted. /// /// See [TextStyle.height], which works in a similar manner. /// /// The default height is null. 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; } /// Returns a new strut style that inherits its null values from /// corresponding properties in the [other] [TextStyle]. /// /// The "missing" properties of the this strut style are _filled_ by /// the properties of the provided [TextStyle]. This is possible because /// [StrutStyle] shares many of the same basic properties as [TextStyle]. /// /// If the given text style is null, returns this strut style. StrutStyle inheritFromTextStyle(TextStyle? other) { if (other == null) return this; return StrutStyle( fontFamily: fontFamily ?? other.fontFamily, fontFamilyFallback: fontFamilyFallback ?? other.fontFamilyFallback, fontSize: fontSize ?? other.fontSize, height: height ?? other.height, leading: leading, // No equivalent property in TextStyle yet. fontWeight: fontWeight ?? other.fontWeight, fontStyle: fontStyle ?? other.fontStyle, forceStrutHeight: forceStrutHeight, // StrutStyle-unique property. debugLabel: debugLabel ?? other.debugLabel, // Package is embedded within the getters for fontFamilyFallback. ); } @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; return other is StrutStyle && other.fontFamily == fontFamily && other.fontSize == fontSize && other.fontWeight == fontWeight && other.fontStyle == fontStyle && other.height == height && other.leading == leading && other.forceStrutHeight == forceStrutHeight; } @override int get hashCode { return hashValues( fontFamily, fontSize, fontWeight, fontStyle, height, leading, forceStrutHeight, ); } @override String toStringShort() => objectRuntimeType(this, 'StrutStyle'); /// 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<DiagnosticsNode> styles = <DiagnosticsNode>[ StringProperty('${prefix}family', fontFamily, defaultValue: null, quoted: false), IterableProperty<String>('${prefix}familyFallback', fontFamilyFallback, defaultValue: null), DoubleProperty('${prefix}size', fontSize, defaultValue: null), ]; String? weightDescription; if (fontWeight != null) { weightDescription = 'w${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<FontWeight>( '${prefix}weight', fontWeight, description: weightDescription, defaultValue: null, )); styles.add(EnumProperty<FontStyle>('${prefix}style', fontStyle, defaultValue: null)); styles.add(DoubleProperty('${prefix}height', height, unit: 'x', defaultValue: null)); styles.add(FlagProperty('${prefix}forceStrutHeight', value: forceStrutHeight, defaultValue: null, ifTrue: '$prefix<strut height forced>', ifFalse: '$prefix<strut height normal>')); 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<strut height forced>', ifFalse: '$prefix<strut height normal>')); } }