blob: cce4e2c50144725d5206689c33ccec7195d5a774 [file] [log] [blame]
// 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 'dart:ui' show TextLeadingDistribution;
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
/// const 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.leadingDistribution,
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,
TextLeadingDistribution? leadingDistribution,
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,
leadingDistribution = leadingDistribution ?? textStyle.leadingDistribution,
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 minimum height of the strut, as a multiple of [fontSize].
///
/// When [height] is omitted or null, then the strut's height will be the sum
/// of the strut's font-defined ascent, its font-defined descent, and its
/// [leading]. 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 strut
/// 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 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;
/// How the vertical space added by the [height] multiplier should be
/// distributed over and under the strut.
///
/// When a non-null [height] is specified, after accommodating the imaginary
/// strut glyph, the remaining vertical space from the allotted
/// `fontSize * height` logical pixels will be distributed over and under the
/// strut, according to the [leadingDistribution] property.
///
/// The additional leading introduced by the [leading] property applies
/// independently of [leadingDistribution]: it will always be distributed
/// evenly over and under the strut, regardless of [leadingDistribution].
///
/// Defaults to null, which defers to the paragraph's
/// `ParagraphStyle.textHeightBehavior`'s [leadingDistribution].
final TextLeadingDistribution? leadingDistribution;
/// 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 additional leading to apply to the strut as a multiple of [fontSize],
/// independent of [height] and [leadingDistribution].
///
/// 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 always equally distributed above and
/// below the baseline, regardless of [leadingDistribution].
///
/// 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 => Object.hash(
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, 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>'));
}
}
}