Commit 67d16cd5 authored by Yegor's avatar Yegor Committed by GitHub

Theme.of provides all TextStyle properties (#12552)

* Theme provides all TextStyle properties

* match field declaration order in the test

* Theme.of returns text style with inherit == false

* change TextStyle.inherit logic; docs

* add TextStyle.debugLabel

* address comments

* add debug labels to Typography text styles
parent faf44b59
...@@ -101,12 +101,16 @@ abstract class MaterialLocalizations { ...@@ -101,12 +101,16 @@ abstract class MaterialLocalizations {
/// ///
/// This text theme is incomplete. For example, it lacks text color /// This text theme is incomplete. For example, it lacks text color
/// information. This theme must be merged with another text theme that /// information. This theme must be merged with another text theme that
/// provides the missing values. The text styles provided by this theme have /// provides the missing values.
/// their [TextStyle.inherit] property set to true.
/// ///
/// Typically a complete theme is obtained via [Theme.of], which can be /// Typically a complete theme is obtained via [Theme.of], which can be
/// localized using the [Localizations] widget. /// localized using the [Localizations] widget.
/// ///
/// The text styles provided by this theme are expected to have their
/// [TextStyle.inherit] property set to false, so that the [ThemeData]
/// obtained from [Theme.of] no longer inherits text style properties and
/// contains a complete set of properties needed to style a [Text] widget.
///
/// See also: https://material.io/guidelines/style/typography.html /// See also: https://material.io/guidelines/style/typography.html
TextTheme get localTextGeometry; TextTheme get localTextGeometry;
......
...@@ -462,11 +462,13 @@ class ThemeData { ...@@ -462,11 +462,13 @@ class ThemeData {
/// Caches localized themes to speed up the [localize] method. /// Caches localized themes to speed up the [localize] method.
static final _FifoCache<_IdentityThemeDataCacheKey, ThemeData> _localizedThemeDataCache = new _FifoCache<_IdentityThemeDataCacheKey, ThemeData>(_localizedThemeDataCacheSize); static final _FifoCache<_IdentityThemeDataCacheKey, ThemeData> _localizedThemeDataCache = new _FifoCache<_IdentityThemeDataCacheKey, ThemeData>(_localizedThemeDataCacheSize);
/// Returns a new theme built by merging [baseTheme] into the text geometry /// Returns a new theme built by merging the text geometry provided by the
/// provided by the [localTextGeometry]. /// [localTextGeometry] theme with the [baseTheme].
/// ///
/// The [TextStyle.inherit] field in the text styles provided by /// For those text styles in the [baseTheme] whose [TextStyle.inherit] is set
/// [localTextGeometry] must be set to true. /// to true, the returned theme's text styles inherit the geometric properties
/// of [localTextGeometry]. The resulting text styles' [TextStyle.inherit] is
/// set to those provided by [localTextGeometry].
static ThemeData localize(ThemeData baseTheme, TextTheme localTextGeometry) { static ThemeData localize(ThemeData baseTheme, TextTheme localTextGeometry) {
// WARNING: this method memoizes the result in a cache based on the // WARNING: this method memoizes the result in a cache based on the
// previously seen baseTheme and localTextGeometry. Memoization is safe // previously seen baseTheme and localTextGeometry. Memoization is safe
......
...@@ -8,6 +8,8 @@ import 'package:flutter/foundation.dart'; ...@@ -8,6 +8,8 @@ import 'package:flutter/foundation.dart';
import 'basic_types.dart'; import 'basic_types.dart';
const String _kDefaultDebugLabel = 'unknown';
/// An immutable style in which paint text. /// An immutable style in which paint text.
/// ///
/// ## Sample code /// ## Sample code
...@@ -230,6 +232,7 @@ class TextStyle extends Diagnosticable { ...@@ -230,6 +232,7 @@ class TextStyle extends Diagnosticable {
this.decoration, this.decoration,
this.decorationColor, this.decorationColor,
this.decorationStyle, this.decorationStyle,
this.debugLabel,
String fontFamily, String fontFamily,
String package, String package,
}) : fontFamily = package == null ? fontFamily : 'packages/$package/$fontFamily', }) : fontFamily = package == null ? fontFamily : 'packages/$package/$fontFamily',
...@@ -302,6 +305,19 @@ class TextStyle extends Diagnosticable { ...@@ -302,6 +305,19 @@ class TextStyle extends Diagnosticable {
/// The style in which to paint the text decorations (e.g., dashed). /// The style in which to paint the text decorations (e.g., dashed).
final TextDecorationStyle decorationStyle; final TextDecorationStyle decorationStyle;
/// A human-readable description of this text style.
///
/// This property is maintained only in debug builds.
///
/// When merging ([merge]), copying ([copyWith]), modifying using [apply], or
/// interpolating ([lerp]), the label of the resulting style is marked with
/// the debug labels of the original styles. This helps figuring out where a
/// particular text style came from.
///
/// This property is not considered when comparing text styles using `==` or
/// [compareTo], and it does not affect [hashCode].
final String debugLabel;
/// Creates a copy of this text style but with the given fields replaced with /// Creates a copy of this text style but with the given fields replaced with
/// the new values. /// the new values.
TextStyle copyWith({ TextStyle copyWith({
...@@ -317,7 +333,14 @@ class TextStyle extends Diagnosticable { ...@@ -317,7 +333,14 @@ class TextStyle extends Diagnosticable {
TextDecoration decoration, TextDecoration decoration,
Color decorationColor, Color decorationColor,
TextDecorationStyle decorationStyle, TextDecorationStyle decorationStyle,
String debugLabel,
}) { }) {
String newDebugLabel;
assert(() {
if (this.debugLabel != null)
newDebugLabel = debugLabel ?? 'copy of ${this.debugLabel}';
return true;
}());
return new TextStyle( return new TextStyle(
inherit: inherit, inherit: inherit,
color: color ?? this.color, color: color ?? this.color,
...@@ -332,11 +355,18 @@ class TextStyle extends Diagnosticable { ...@@ -332,11 +355,18 @@ class TextStyle extends Diagnosticable {
decoration: decoration ?? this.decoration, decoration: decoration ?? this.decoration,
decorationColor: decorationColor ?? this.decorationColor, decorationColor: decorationColor ?? this.decorationColor,
decorationStyle: decorationStyle ?? this.decorationStyle, decorationStyle: decorationStyle ?? this.decorationStyle,
debugLabel: newDebugLabel,
); );
} }
/// Creates a copy of this text style but with the numeric fields multiplied /// Creates a copy of this text style replacing or altering the specified
/// by the given factors and then incremented by the given deltas. /// properties.
///
/// The non-numeric properties [color], [fontFamily], [decoration],
/// [decorationColor] and [decorationStyle] are replaced with the new values.
///
/// The numeric properties are multiplied by the given factors and then
/// incremented by the given deltas.
/// ///
/// For example, `style.apply(fontSizeFactor: 2.0, fontSizeDelta: 1.0)` would /// For example, `style.apply(fontSizeFactor: 2.0, fontSizeDelta: 1.0)` would
/// return a [TextStyle] whose [fontSize] is `style.fontSize * 2.0 + 1.0`. /// return a [TextStyle] whose [fontSize] is `style.fontSize * 2.0 + 1.0`.
...@@ -346,14 +376,15 @@ class TextStyle extends Diagnosticable { ...@@ -346,14 +376,15 @@ class TextStyle extends Diagnosticable {
/// applied to a `style` whose [fontWeight] is [FontWeight.w500] will return a /// applied to a `style` whose [fontWeight] is [FontWeight.w500] will return a
/// [TextStyle] with a [FontWeight.w300]. /// [TextStyle] with a [FontWeight.w300].
/// ///
/// The arguments must not be null. /// The numeric arguments must not be null.
/// ///
/// If the underlying values are null, then the corresponding factors and/or /// If the underlying values are null, then the corresponding factors and/or
/// deltas must not be specified. /// deltas must not be specified.
///
/// The non-numeric fields can be controlled using the corresponding arguments.
TextStyle apply({ TextStyle apply({
Color color, Color color,
TextDecoration decoration,
Color decorationColor,
TextDecorationStyle decorationStyle,
String fontFamily, String fontFamily,
double fontSizeFactor: 1.0, double fontSizeFactor: 1.0,
double fontSizeDelta: 0.0, double fontSizeDelta: 0.0,
...@@ -379,6 +410,14 @@ class TextStyle extends Diagnosticable { ...@@ -379,6 +410,14 @@ class TextStyle extends Diagnosticable {
assert(heightFactor != null); assert(heightFactor != null);
assert(heightDelta != null); assert(heightDelta != null);
assert(heightFactor != null || (heightFactor == 1.0 && heightDelta == 0.0)); assert(heightFactor != null || (heightFactor == 1.0 && heightDelta == 0.0));
String modifiedDebugLabel;
assert(() {
if (debugLabel != null)
modifiedDebugLabel = 'modified $debugLabel';
return true;
}());
return new TextStyle( return new TextStyle(
inherit: inherit, inherit: inherit,
color: color ?? this.color, color: color ?? this.color,
...@@ -390,19 +429,40 @@ class TextStyle extends Diagnosticable { ...@@ -390,19 +429,40 @@ class TextStyle extends Diagnosticable {
wordSpacing: wordSpacing == null ? null : wordSpacing * wordSpacingFactor + wordSpacingDelta, wordSpacing: wordSpacing == null ? null : wordSpacing * wordSpacingFactor + wordSpacingDelta,
textBaseline: textBaseline, textBaseline: textBaseline,
height: height == null ? null : height * heightFactor + heightDelta, height: height == null ? null : height * heightFactor + heightDelta,
decoration: decoration, decoration: decoration ?? this.decoration,
decorationColor: decorationColor, decorationColor: decorationColor ?? this.decorationColor,
decorationStyle: decorationStyle, decorationStyle: decorationStyle ?? this.decorationStyle,
debugLabel: modifiedDebugLabel,
); );
} }
/// Returns a new text style that matches this text style but with some values /// Returns a new text style that is a combination of this style and the given
/// replaced by the non-null parameters of the given text style. If the given /// [other] style.
/// text style is null, simply returns this text style. ///
/// If the given [other] text style has its [TextStyle.inherit] set to true,
/// its null properties are replaced with the non-null properties of this text
/// style. The [other] style _inherits_ the properties of this style. Another
/// way to think of it is that the "missing" properties of the [other] style
/// are _filled_ by the properties of this style.
///
/// If the given [other] text style has its [TextStyle.inherit] set to false,
/// returns the given [other] style unchanged. The [other] style does not
/// inherit properties of this style.
///
/// If the given text style is null, returns this text style.
TextStyle merge(TextStyle other) { TextStyle merge(TextStyle other) {
if (other == null) if (other == null)
return this; return this;
assert(other.inherit); if (!other.inherit)
return other;
String mergedDebugLabel;
assert(() {
if (other.debugLabel != null || debugLabel != null)
mergedDebugLabel = '${other.debugLabel ?? _kDefaultDebugLabel} < ${debugLabel ?? _kDefaultDebugLabel}';
return true;
}());
return copyWith( return copyWith(
color: other.color, color: other.color,
fontFamily: other.fontFamily, fontFamily: other.fontFamily,
...@@ -415,7 +475,8 @@ class TextStyle extends Diagnosticable { ...@@ -415,7 +475,8 @@ class TextStyle extends Diagnosticable {
height: other.height, height: other.height,
decoration: other.decoration, decoration: other.decoration,
decorationColor: other.decorationColor, decorationColor: other.decorationColor,
decorationStyle: other.decorationStyle decorationStyle: other.decorationStyle,
debugLabel: mergedDebugLabel,
); );
} }
...@@ -424,6 +485,13 @@ class TextStyle extends Diagnosticable { ...@@ -424,6 +485,13 @@ class TextStyle extends Diagnosticable {
/// This will not work well if the styles don't set the same fields. /// This will not work well if the styles don't set the same fields.
static TextStyle lerp(TextStyle begin, TextStyle end, double t) { static TextStyle lerp(TextStyle begin, TextStyle end, double t) {
assert(begin.inherit == end.inherit); assert(begin.inherit == end.inherit);
String lerpDebugLabel;
assert(() {
lerpDebugLabel = 'lerp(${begin.debugLabel ?? _kDefaultDebugLabel}, ${end.debugLabel ?? _kDefaultDebugLabel})';
return true;
}());
return new TextStyle( return new TextStyle(
inherit: end.inherit, inherit: end.inherit,
color: Color.lerp(begin.color, end.color, t), color: Color.lerp(begin.color, end.color, t),
...@@ -437,7 +505,8 @@ class TextStyle extends Diagnosticable { ...@@ -437,7 +505,8 @@ class TextStyle extends Diagnosticable {
height: ui.lerpDouble(begin.height ?? end.height, end.height ?? begin.height, t), height: ui.lerpDouble(begin.height ?? end.height, end.height ?? begin.height, t),
decoration: t < 0.5 ? begin.decoration : end.decoration, decoration: t < 0.5 ? begin.decoration : end.decoration,
decorationColor: Color.lerp(begin.decorationColor, end.decorationColor, t), decorationColor: Color.lerp(begin.decorationColor, end.decorationColor, t),
decorationStyle: t < 0.5 ? begin.decorationStyle : end.decorationStyle decorationStyle: t < 0.5 ? begin.decorationStyle : end.decorationStyle,
debugLabel: lerpDebugLabel,
); );
} }
...@@ -564,6 +633,8 @@ class TextStyle extends Diagnosticable { ...@@ -564,6 +633,8 @@ class TextStyle extends Diagnosticable {
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties, { String prefix: '' }) { void debugFillProperties(DiagnosticPropertiesBuilder properties, { String prefix: '' }) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
if (debugLabel != null)
properties.add(new MessageProperty('${prefix}debugLabel', debugLabel));
final List<DiagnosticsNode> styles = <DiagnosticsNode>[]; final List<DiagnosticsNode> styles = <DiagnosticsNode>[];
styles.add(new DiagnosticsProperty<Color>('${prefix}color', color, defaultValue: null)); styles.add(new DiagnosticsProperty<Color>('${prefix}color', color, defaultValue: null));
styles.add(new StringProperty('${prefix}family', fontFamily, defaultValue: null, quoted: false)); styles.add(new StringProperty('${prefix}family', fontFamily, defaultValue: null, quoted: false));
......
...@@ -795,13 +795,7 @@ void main() { ...@@ -795,13 +795,7 @@ void main() {
}); });
testWidgets('TextField with default helperStyle', (WidgetTester tester) async { testWidgets('TextField with default helperStyle', (WidgetTester tester) async {
final ThemeData themeData = ThemeData.localize( final ThemeData themeData = new ThemeData(hintColor: Colors.blue[500]);
new ThemeData(
hintColor: Colors.blue[500],
),
MaterialTextGeometry.forScriptCategory(MaterialTextGeometry.englishLikeCategory),
);
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: new Theme( child: new Theme(
...@@ -816,7 +810,7 @@ void main() { ...@@ -816,7 +810,7 @@ void main() {
); );
final Text helperText = tester.widget(find.text('helper text')); final Text helperText = tester.widget(find.text('helper text'));
expect(helperText.style.color, themeData.hintColor); expect(helperText.style.color, themeData.hintColor);
expect(helperText.style.fontSize, themeData.textTheme.caption.fontSize); expect(helperText.style.fontSize, MaterialTextGeometry.englishLike.caption.fontSize);
}); });
testWidgets('TextField with specified helperStyle', (WidgetTester tester) async { testWidgets('TextField with specified helperStyle', (WidgetTester tester) async {
......
...@@ -24,7 +24,8 @@ void main() { ...@@ -24,7 +24,8 @@ void main() {
for (TargetPlatform platform in TargetPlatform.values) { for (TargetPlatform platform in TargetPlatform.values) {
final ThemeData theme = new ThemeData(platform: platform); final ThemeData theme = new ThemeData(platform: platform);
final Typography typography = new Typography(platform: platform); final Typography typography = new Typography(platform: platform);
expect(theme.textTheme, typography.black, reason: 'Not using default typography for $platform'); expect(theme.textTheme, typography.black.apply(decoration: TextDecoration.none),
reason: 'Not using default typography for $platform');
} }
}); });
......
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/src/foundation/diagnostics.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
...@@ -367,6 +369,56 @@ void main() { ...@@ -367,6 +369,56 @@ void main() {
expect(actualFontSize, _kMagicFontSize); expect(actualFontSize, _kMagicFontSize);
}); });
testWidgets('Default Theme provides all basic TextStyle properties', (WidgetTester tester) async {
ThemeData theme;
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new Builder(
builder: (BuildContext context) {
theme = Theme.of(context);
return const Text('A');
},
),
));
List<TextStyle> extractStyles(TextTheme textTheme) {
return <TextStyle>[
textTheme.display4,
textTheme.display3,
textTheme.display2,
textTheme.display1,
textTheme.headline,
textTheme.title,
textTheme.subhead,
textTheme.body2,
textTheme.body1,
textTheme.caption,
textTheme.button,
];
}
for (TextTheme textTheme in <TextTheme>[theme.textTheme, theme.primaryTextTheme, theme.accentTextTheme]) {
for (TextStyle style in extractStyles(textTheme).map((TextStyle style) => new _TextStyleProxy(style))) {
expect(style.inherit, false);
expect(style.color, isNotNull);
expect(style.fontFamily, isNotNull);
expect(style.fontSize, isNotNull);
expect(style.fontWeight, isNotNull);
expect(style.fontStyle, null);
expect(style.letterSpacing, null);
expect(style.wordSpacing, null);
expect(style.textBaseline, isNotNull);
expect(style.height, null);
expect(style.decoration, TextDecoration.none);
expect(style.decorationColor, null);
expect(style.decorationStyle, null);
expect(style.debugLabel, isNotNull);
}
}
expect(theme.textTheme.display4.debugLabel, 'blackMountainView display4 < englishLike display4');
});
} }
int testBuildCalled; int testBuildCalled;
...@@ -388,3 +440,73 @@ class _TestState extends State<Test> { ...@@ -388,3 +440,73 @@ class _TestState extends State<Test> {
); );
} }
} }
/// This class exists only to make sure that we test all the properties of the
/// [TextStyle] class. If a property is added/removed/renamed, the analyzer will
/// complain that this class has incorrect overrides.
class _TextStyleProxy implements TextStyle {
_TextStyleProxy(this._delegate);
final TextStyle _delegate;
// Do make sure that all the properties correctly forward to the _delegate.
@override Color get color => _delegate.color;
@override String get debugLabel => _delegate.debugLabel;
@override TextDecoration get decoration => _delegate.decoration;
@override Color get decorationColor => _delegate.decorationColor;
@override TextDecorationStyle get decorationStyle => _delegate.decorationStyle;
@override String get fontFamily => _delegate.fontFamily;
@override double get fontSize => _delegate.fontSize;
@override FontStyle get fontStyle => _delegate.fontStyle;
@override FontWeight get fontWeight => _delegate.fontWeight;
@override double get height => _delegate.height;
@override bool get inherit => _delegate.inherit;
@override double get letterSpacing => _delegate.letterSpacing;
@override TextBaseline get textBaseline => _delegate.textBaseline;
@override double get wordSpacing => _delegate.wordSpacing;
@override
DiagnosticsNode toDiagnosticsNode({String name, DiagnosticsTreeStyle style}) {
throw new UnimplementedError();
}
@override
String toStringShort() {
throw new UnimplementedError();
}
@override
TextStyle apply({Color color, TextDecoration decoration, Color decorationColor, TextDecorationStyle decorationStyle, String fontFamily, double fontSizeFactor: 1.0, double fontSizeDelta: 0.0, int fontWeightDelta: 0, double letterSpacingFactor: 1.0, double letterSpacingDelta: 0.0, double wordSpacingFactor: 1.0, double wordSpacingDelta: 0.0, double heightFactor: 1.0, double heightDelta: 0.0}) {
throw new UnimplementedError();
}
@override
RenderComparison compareTo(TextStyle other) {
throw new UnimplementedError();
}
@override
TextStyle copyWith({Color color, String fontFamily, double fontSize, FontWeight fontWeight, FontStyle fontStyle, double letterSpacing, double wordSpacing, TextBaseline textBaseline, double height, TextDecoration decoration, Color decorationColor, TextDecorationStyle decorationStyle, String debugLabel}) {
throw new UnimplementedError();
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties, {String prefix: ''}) {
throw new UnimplementedError();
}
@override
ui.ParagraphStyle getParagraphStyle({TextAlign textAlign, TextDirection textDirection, double textScaleFactor: 1.0, String ellipsis, int maxLines}) {
throw new UnimplementedError();
}
@override
ui.TextStyle getTextStyle({double textScaleFactor: 1.0}) {
throw new UnimplementedError();
}
@override
TextStyle merge(TextStyle other) {
throw new UnimplementedError();
}
}
...@@ -142,4 +142,25 @@ void main() { ...@@ -142,4 +142,25 @@ void main() {
expect(s7.fontFamily, 'packages/p/test'); expect(s7.fontFamily, 'packages/p/test');
expect(s7.getTextStyle().toString(), 'TextStyle(color: unspecified, decoration: unspecified, decorationColor: unspecified, decorationStyle: unspecified, fontWeight: unspecified, fontStyle: unspecified, textBaseline: unspecified, fontFamily: packages/p/test, fontSize: unspecified, letterSpacing: unspecified, wordSpacing: unspecified, height: unspecified)'); expect(s7.getTextStyle().toString(), 'TextStyle(color: unspecified, decoration: unspecified, decorationColor: unspecified, decorationStyle: unspecified, fontWeight: unspecified, fontStyle: unspecified, textBaseline: unspecified, fontFamily: packages/p/test, fontSize: unspecified, letterSpacing: unspecified, wordSpacing: unspecified, height: unspecified)');
}); });
test('TextStyle.debugLabel', () {
const TextStyle unknown = const TextStyle();
const TextStyle foo = const TextStyle(debugLabel: 'foo', fontSize: 1.0);
const TextStyle bar = const TextStyle(debugLabel: 'bar', fontSize: 2.0);
const TextStyle baz = const TextStyle(debugLabel: 'baz', fontSize: 3.0);
expect(unknown.debugLabel, null);
expect(unknown.toString(), 'TextStyle(<all styles inherited>)');
expect(unknown.copyWith().debugLabel, null);
expect(unknown.apply().debugLabel, null);
expect(foo.debugLabel, 'foo');
expect(foo.toString(), 'TextStyle(debugLabel: foo, inherit: true, size: 1.0)');
expect(foo.merge(bar).debugLabel, 'bar < foo');
expect(foo.merge(bar).merge(baz).debugLabel, 'baz < bar < foo');
expect(foo.copyWith().debugLabel, 'copy of foo');
expect(foo.apply().debugLabel, 'modified foo');
expect(TextStyle.lerp(foo, bar, 0.5).debugLabel, 'lerp(foo, bar)');
expect(TextStyle.lerp(foo.merge(bar), baz, 0.5).copyWith().debugLabel, 'copy of lerp(bar < foo, baz)');
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment