Unverified Commit be3802c9 authored by Pierre-Louis's avatar Pierre-Louis Committed by GitHub

Add support for fill, weight, grade, and optical size to `Icon` (#106896)

* wip

* update documentation

* x

* remove trailing spaces

* x

* remove useless CupertinoIconThemeData copyWith override

* add tests

* remove trailing spaces

* fix isConcrete

* x

* x

* x

* remove trailing spaces

* tweak docs

* mention that font filenames often indicate the supported axes

* add back cupertino IconThemeData copyWith

* update copyWith
parent bfcceeb3
...@@ -10,13 +10,14 @@ import 'colors.dart'; ...@@ -10,13 +10,14 @@ import 'colors.dart';
/// using [IconTheme.of]. /// using [IconTheme.of].
class CupertinoIconThemeData extends IconThemeData with Diagnosticable { class CupertinoIconThemeData extends IconThemeData with Diagnosticable {
/// Creates a [CupertinoIconThemeData]. /// Creates a [CupertinoIconThemeData].
///
/// The opacity applies to both explicit and default icon colors. The value
/// is clamped between 0.0 and 1.0.
const CupertinoIconThemeData({ const CupertinoIconThemeData({
super.size,
super.fill,
super.weight,
super.grade,
super.opticalSize,
super.color, super.color,
super.opacity, super.opacity,
super.size,
super.shadows, super.shadows,
}); });
...@@ -30,11 +31,24 @@ class CupertinoIconThemeData extends IconThemeData with Diagnosticable { ...@@ -30,11 +31,24 @@ class CupertinoIconThemeData extends IconThemeData with Diagnosticable {
/// Creates a copy of this icon theme but with the given fields replaced with /// Creates a copy of this icon theme but with the given fields replaced with
/// the new values. /// the new values.
@override @override
CupertinoIconThemeData copyWith({ Color? color, double? opacity, double? size, List<Shadow>? shadows }) { CupertinoIconThemeData copyWith({
double? size,
double? fill,
double? weight,
double? grade,
double? opticalSize,
Color? color,
double? opacity,
List<Shadow>? shadows,
}) {
return CupertinoIconThemeData( return CupertinoIconThemeData(
size: size ?? this.size,
fill: fill ?? this.fill,
weight: weight ?? this.weight,
grade: grade ?? this.grade,
opticalSize: opticalSize ?? this.opticalSize,
color: color ?? this.color, color: color ?? this.color,
opacity: opacity ?? this.opacity, opacity: opacity ?? this.opacity,
size: size ?? this.size,
shadows: shadows ?? this.shadows, shadows: shadows ?? this.shadows,
); );
} }
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// 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';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -61,22 +63,26 @@ import 'icon_theme_data.dart'; ...@@ -61,22 +63,26 @@ import 'icon_theme_data.dart';
/// See also: /// See also:
/// ///
/// * [IconButton], for interactive icons. /// * [IconButton], for interactive icons.
/// * [Icons], the library of Material Icons available for use with this class. /// * [Icons], for the list of available Material Icons for use with this class.
/// * [IconTheme], which provides ambient configuration for icons. /// * [IconTheme], which provides ambient configuration for icons.
/// * [ImageIcon], for showing icons from [AssetImage]s or other [ImageProvider]s. /// * [ImageIcon], for showing icons from [AssetImage]s or other [ImageProvider]s.
class Icon extends StatelessWidget { class Icon extends StatelessWidget {
/// Creates an icon. /// Creates an icon.
///
/// The [size] and [color] default to the value given by the current [IconTheme].
const Icon( const Icon(
this.icon, { this.icon, {
super.key, super.key,
this.size, this.size,
this.fill,
this.weight,
this.grade,
this.opticalSize,
this.color, this.color,
this.shadows,
this.semanticLabel, this.semanticLabel,
this.textDirection, this.textDirection,
this.shadows, }) : assert(fill == null || (0.0 <= fill && fill <= 1.0)),
}); assert(weight == null || (0.0 < weight)),
assert(opticalSize == null || (0.0 < opticalSize));
/// The icon to display. The available icons are described in [Icons]. /// The icon to display. The available icons are described in [Icons].
/// ///
...@@ -88,9 +94,7 @@ class Icon extends StatelessWidget { ...@@ -88,9 +94,7 @@ class Icon extends StatelessWidget {
/// ///
/// Icons occupy a square with width and height equal to size. /// Icons occupy a square with width and height equal to size.
/// ///
/// Defaults to the current [IconTheme] size, if any. If there is no /// Defaults to the nearest [IconTheme]'s [IconThemeData.size].
/// [IconTheme], or it does not specify an explicit size, then it defaults to
/// 24.0.
/// ///
/// If this [Icon] is being placed inside an [IconButton], then use /// If this [Icon] is being placed inside an [IconButton], then use
/// [IconButton.iconSize] instead, so that the [IconButton] can make the splash /// [IconButton.iconSize] instead, so that the [IconButton] can make the splash
...@@ -98,23 +102,87 @@ class Icon extends StatelessWidget { ...@@ -98,23 +102,87 @@ class Icon extends StatelessWidget {
/// pass down the size to the [Icon]. /// pass down the size to the [Icon].
final double? size; final double? size;
/// The color to use when drawing the icon. /// The fill for drawing the icon.
/// ///
/// Defaults to the current [IconTheme] color, if any. /// Requires the underlying icon font to support the `FILL` [FontVariation]
/// axis, otherwise has no effect. Variable font filenames often indicate
/// the supported axes. Must be between 0.0 (unfilled) and 1.0 (filled),
/// inclusive.
/// ///
/// The color (whether specified explicitly here or obtained from the /// Can be used to convey a state transition for animation or interaction.
/// [IconTheme]) will be further adjusted by the opacity of the current ///
/// [IconTheme], if any. /// Defaults to nearest [IconTheme]'s [IconThemeData.fill].
///
/// See also:
/// * [weight], for controlling stroke weight.
/// * [grade], for controlling stroke weight in a more granular way.
/// * [opticalSize], for controlling optical size.
final double? fill;
/// The stroke weight for drawing the icon.
///
/// Requires the underlying icon font to support the `wght` [FontVariation]
/// axis, otherwise has no effect. Variable font filenames often indicate
/// the supported axes. Must be greater than 0.
///
/// Defaults to nearest [IconTheme]'s [IconThemeData.weight].
///
/// See also:
/// * [fill], for controlling fill.
/// * [grade], for controlling stroke weight in a more granular way.
/// * [opticalSize], for controlling optical size.
/// * https://fonts.google.com/knowledge/glossary/weight_axis
final double? weight;
/// The grade (granular stroke weight) for drawing the icon.
///
/// Requires the underlying icon font to support the `GRAD` [FontVariation]
/// axis, otherwise has no effect. Variable font filenames often indicate
/// the supported axes. Can be negative.
///
/// Grade and [weight] both affect a symbol's stroke weight (thickness), but
/// grade has a smaller impact on the size of the symbol.
///
/// Grade is also available in some text fonts. One can match grade levels
/// between text and symbols for a harmonious visual effect. For example, if
/// the text font has a -25 grade value, the symbols can match it with a
/// suitable value, say -25.
///
/// Defaults to nearest [IconTheme]'s [IconThemeData.grade].
///
/// See also:
/// * [fill], for controlling fill.
/// * [weight], for controlling stroke weight in a less granular way.
/// * [opticalSize], for controlling optical size.
/// * https://fonts.google.com/knowledge/glossary/grade_axis
final double? grade;
/// The optical size for drawing the icon.
///
/// Requires the underlying icon font to support the `opsz` [FontVariation]
/// axis, otherwise has no effect. Variable font filenames often indicate
/// the supported axes. Must be greater than 0.
///
/// For an icon to look the same at different sizes, the stroke weight
/// (thickness) must change as the icon size scales. Optical size offers a way
/// to automatically adjust the stroke weight as icon size changes.
///
/// Defaults to nearest [IconTheme]'s [IconThemeData.opticalSize].
/// ///
/// In material apps, if there is a [Theme] without any [IconTheme]s /// See also:
/// specified, icon colors default to white if the theme is dark /// * [fill], for controlling fill.
/// and black if the theme is light. /// * [weight], for controlling stroke weight.
/// * [grade], for controlling stroke weight in a more granular way.
/// * https://fonts.google.com/knowledge/glossary/optical_size_axis
final double? opticalSize;
/// The color to use when drawing the icon.
/// ///
/// If no [IconTheme] and no [Theme] is specified, icons will default to /// Defaults to the nearest [IconTheme]'s [IconThemeData.color].
/// black.
/// ///
/// See [Theme] to set the current theme and [ThemeData.brightness] /// The color (whether specified explicitly here or obtained from the
/// for setting the current theme's brightness. /// [IconTheme]) will be further adjusted by the nearest [IconTheme]'s
/// [IconThemeData.opacity].
/// ///
/// {@tool snippet} /// {@tool snippet}
/// Typically, a Material Design color will be used, as follows: /// Typically, a Material Design color will be used, as follows:
...@@ -128,6 +196,17 @@ class Icon extends StatelessWidget { ...@@ -128,6 +196,17 @@ class Icon extends StatelessWidget {
/// {@end-tool} /// {@end-tool}
final Color? color; final Color? color;
/// A list of [Shadow]s that will be painted underneath the icon.
///
/// Multiple shadows are supported to replicate lighting from multiple light
/// sources.
///
/// Shadows must be in the same order for [Icon] to be considered as
/// equivalent as order produces differing transparency.
///
/// Defaults to the nearest [IconTheme]'s [IconThemeData.shadows].
final List<Shadow>? shadows;
/// Semantic label for the icon. /// Semantic label for the icon.
/// ///
/// Announced in accessibility modes (e.g TalkBack/VoiceOver). /// Announced in accessibility modes (e.g TalkBack/VoiceOver).
...@@ -152,15 +231,6 @@ class Icon extends StatelessWidget { ...@@ -152,15 +231,6 @@ class Icon extends StatelessWidget {
/// specified, either directly using this property or using [Directionality]. /// specified, either directly using this property or using [Directionality].
final TextDirection? textDirection; final TextDirection? textDirection;
/// A list of [Shadow]s that will be painted underneath the icon.
///
/// Multiple shadows are supported to replicate lighting from multiple light
/// sources.
///
/// Shadows must be in the same order for [Icon] to be considered as
/// equivalent as order produces differing transparency.
final List<Shadow>? shadows;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(this.textDirection != null || debugCheckHasDirectionality(context)); assert(this.textDirection != null || debugCheckHasDirectionality(context));
...@@ -191,6 +261,12 @@ class Icon extends StatelessWidget { ...@@ -191,6 +261,12 @@ class Icon extends StatelessWidget {
text: TextSpan( text: TextSpan(
text: String.fromCharCode(icon!.codePoint), text: String.fromCharCode(icon!.codePoint),
style: TextStyle( style: TextStyle(
fontVariations: <FontVariation>[
if (fill != null) FontVariation('FILL', fill!),
if (weight != null) FontVariation('wght', weight!),
if (grade != null) FontVariation('GRAD', grade!),
if (opticalSize != null) FontVariation('opsz', opticalSize!),
],
inherit: false, inherit: false,
color: iconColor, color: iconColor,
fontSize: iconSize, fontSize: iconSize,
...@@ -235,7 +311,13 @@ class Icon extends StatelessWidget { ...@@ -235,7 +311,13 @@ class Icon extends StatelessWidget {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(IconDataProperty('icon', icon, ifNull: '<empty>', showName: false)); properties.add(IconDataProperty('icon', icon, ifNull: '<empty>', showName: false));
properties.add(DoubleProperty('size', size, defaultValue: null)); properties.add(DoubleProperty('size', size, defaultValue: null));
properties.add(DoubleProperty('fill', fill, defaultValue: null));
properties.add(DoubleProperty('weight', weight, defaultValue: null));
properties.add(DoubleProperty('grade', grade, defaultValue: null));
properties.add(DoubleProperty('opticalSize', opticalSize, defaultValue: null));
properties.add(ColorProperty('color', color, defaultValue: null)); properties.add(ColorProperty('color', color, defaultValue: null));
properties.add(IterableProperty<Shadow>('shadows', shadows, defaultValue: null)); properties.add(IterableProperty<Shadow>('shadows', shadows, defaultValue: null));
properties.add(StringProperty('semanticLabel', semanticLabel, defaultValue: null));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
} }
} }
...@@ -9,12 +9,11 @@ import 'framework.dart'; ...@@ -9,12 +9,11 @@ import 'framework.dart';
import 'icon_theme_data.dart'; import 'icon_theme_data.dart';
import 'inherited_theme.dart'; import 'inherited_theme.dart';
/// Controls the default color, opacity, and size of icons in a widget subtree. /// Controls the default properties of icons in a widget subtree.
/// ///
/// The icon theme is honored by [Icon] and [ImageIcon] widgets. /// The icon theme is honored by [Icon] and [ImageIcon] widgets.
class IconTheme extends InheritedTheme { class IconTheme extends InheritedTheme {
/// Creates an icon theme that controls the color, opacity, and size of /// Creates an icon theme that controls properties of descendant widgets.
/// descendant widgets.
/// ///
/// Both [data] and [child] arguments must not be null. /// Both [data] and [child] arguments must not be null.
const IconTheme({ const IconTheme({
...@@ -24,7 +23,7 @@ class IconTheme extends InheritedTheme { ...@@ -24,7 +23,7 @@ class IconTheme extends InheritedTheme {
}) : assert(data != null), }) : assert(data != null),
assert(child != null); assert(child != null);
/// Creates an icon theme that controls the color, opacity, and size of /// Creates an icon theme that controls the properties of
/// descendant widgets, and merges in the current icon theme, if any. /// descendant widgets, and merges in the current icon theme, if any.
/// ///
/// The [data] and [child] arguments must not be null. /// The [data] and [child] arguments must not be null.
...@@ -44,7 +43,7 @@ class IconTheme extends InheritedTheme { ...@@ -44,7 +43,7 @@ class IconTheme extends InheritedTheme {
); );
} }
/// The color, opacity, and size to use for icons in this subtree. /// The set of properties to use for icons in this subtree.
final IconThemeData data; final IconThemeData data;
/// The data from the closest instance of this class that encloses the given /// The data from the closest instance of this class that encloses the given
...@@ -72,6 +71,10 @@ class IconTheme extends InheritedTheme { ...@@ -72,6 +71,10 @@ class IconTheme extends InheritedTheme {
? iconThemeData ? iconThemeData
: iconThemeData.copyWith( : iconThemeData.copyWith(
size: iconThemeData.size ?? const IconThemeData.fallback().size, size: iconThemeData.size ?? const IconThemeData.fallback().size,
fill: iconThemeData.fill ?? const IconThemeData.fallback().fill,
weight: iconThemeData.weight ?? const IconThemeData.fallback().weight,
grade: iconThemeData.grade ?? const IconThemeData.fallback().grade,
opticalSize: iconThemeData.opticalSize ?? const IconThemeData.fallback().opticalSize,
color: iconThemeData.color ?? const IconThemeData.fallback().color, color: iconThemeData.color ?? const IconThemeData.fallback().color,
opacity: iconThemeData.opacity ?? const IconThemeData.fallback().opacity, opacity: iconThemeData.opacity ?? const IconThemeData.fallback().opacity,
shadows: iconThemeData.shadows ?? const IconThemeData.fallback().shadows, shadows: iconThemeData.shadows ?? const IconThemeData.fallback().shadows,
......
...@@ -9,38 +9,67 @@ import 'package:flutter/painting.dart'; ...@@ -9,38 +9,67 @@ import 'package:flutter/painting.dart';
import 'framework.dart' show BuildContext; import 'framework.dart' show BuildContext;
/// Defines the color, opacity, and size of icons. /// Defines the size, font variations, color, opacity, and shadows of icons.
/// ///
/// Used by [IconTheme] to control the color, opacity, and size of icons in a /// Used by [IconTheme] to control those properties in a widget subtree.
/// widget subtree.
/// ///
/// To obtain the current icon theme, use [IconTheme.of]. To convert an icon /// To obtain the current icon theme, use [IconTheme.of]. To convert an icon
/// theme to a version with all the fields filled in, use [ /// theme to a version with all the fields filled in, use
/// IconThemeData.fallback]. /// [IconThemeData.fallback].
@immutable @immutable
class IconThemeData with Diagnosticable { class IconThemeData with Diagnosticable {
/// Creates an icon theme data. /// Creates an icon theme data.
/// ///
/// The opacity applies to both explicit and default icon colors. The value /// The opacity applies to both explicit and default icon colors. The value
/// is clamped between 0.0 and 1.0. /// is clamped between 0.0 and 1.0.
const IconThemeData({this.color, double? opacity, this.size, this.shadows}) : _opacity = opacity; const IconThemeData({
this.size,
this.fill,
this.weight,
this.grade,
this.opticalSize,
this.color,
double? opacity,
this.shadows,
}) : _opacity = opacity,
assert(fill == null || (0.0 <= fill && fill <= 1.0)),
assert(weight == null || (0.0 < weight)),
assert(opticalSize == null || (0.0 < opticalSize));
/// Creates an icon theme with some reasonable default values. /// Creates an icon theme with some reasonable default values.
/// ///
/// The [color] is black, the [opacity] is 1.0, and the [size] is 24.0. /// The [size] is 24.0, [fill] is 0.0, [weight] is 400.0, [grade] is 0.0,
/// opticalSize is 48.0, [color] is black, and [opacity] is 1.0.
const IconThemeData.fallback() const IconThemeData.fallback()
: color = const Color(0xFF000000), : size = 24.0,
fill = 0.0,
weight = 400.0,
grade = 0.0,
opticalSize = 48.0,
color = const Color(0xFF000000),
_opacity = 1.0, _opacity = 1.0,
size = 24.0,
shadows = null; shadows = null;
/// Creates a copy of this icon theme but with the given fields replaced with /// Creates a copy of this icon theme but with the given fields replaced with
/// the new values. /// the new values.
IconThemeData copyWith({Color? color, double? opacity, double? size, List<Shadow>? shadows}) { IconThemeData copyWith({
double? size,
double? fill,
double? weight,
double? grade,
double? opticalSize,
Color? color,
double? opacity,
List<Shadow>? shadows,
}) {
return IconThemeData( return IconThemeData(
size: size ?? this.size,
fill: fill ?? this.fill,
weight: weight ?? this.weight,
grade: grade ?? this.grade,
opticalSize: opticalSize ?? this.opticalSize,
color: color ?? this.color, color: color ?? this.color,
opacity: opacity ?? this.opacity, opacity: opacity ?? this.opacity,
size: size ?? this.size,
shadows: shadows ?? this.shadows, shadows: shadows ?? this.shadows,
); );
} }
...@@ -53,9 +82,13 @@ class IconThemeData with Diagnosticable { ...@@ -53,9 +82,13 @@ class IconThemeData with Diagnosticable {
return this; return this;
} }
return copyWith( return copyWith(
size: other.size,
fill: other.fill,
weight: other.weight,
grade: other.grade,
opticalSize: other.opticalSize,
color: other.color, color: other.color,
opacity: other.opacity, opacity: other.opacity,
size: other.size,
shadows: other.shadows, shadows: other.shadows,
); );
} }
...@@ -78,20 +111,56 @@ class IconThemeData with Diagnosticable { ...@@ -78,20 +111,56 @@ class IconThemeData with Diagnosticable {
/// the color of [CupertinoIconThemeData] before returning. /// the color of [CupertinoIconThemeData] before returning.
IconThemeData resolve(BuildContext context) => this; IconThemeData resolve(BuildContext context) => this;
/// Whether all the properties of this object are non-null. /// Whether all the properties (except shadows) of this object are non-null.
bool get isConcrete => color != null && opacity != null && size != null; bool get isConcrete => size != null
&& fill != null
&& weight != null
&& grade != null
&& opticalSize != null
&& color != null
&& opacity != null;
/// The default for [Icon.size].
///
/// Falls back to 24.0.
final double? size;
/// The default for [Icon.fill].
///
/// Falls back to 0.0.
final double? fill;
/// The default for [Icon.weight].
///
/// Falls back to 400.0.
final double? weight;
/// The default for [Icon.grade].
///
/// Falls back to 0.0.
final double? grade;
/// The default for [Icon.opticalSize].
///
/// Falls back to 48.0.
final double? opticalSize;
/// The default color for icons. /// The default for [Icon.color].
///
/// In material apps, if there is a [Theme] without any [IconTheme]s
/// specified, icon colors default to white if [ThemeData.brightness] is dark
/// and black if [ThemeData.brightness] is light.
///
/// Otherwise, falls back to black.
final Color? color; final Color? color;
/// An opacity to apply to both explicit and default icon colors. /// An opacity to apply to both explicit and default icon colors.
///
/// Falls back to 1.0.
double? get opacity => _opacity == null ? null : clampDouble(_opacity!, 0.0, 1.0); double? get opacity => _opacity == null ? null : clampDouble(_opacity!, 0.0, 1.0);
final double? _opacity; final double? _opacity;
/// The default size for icons. /// The default for [Icon.shadows].
final double? size;
/// The default shadow for icons.
final List<Shadow>? shadows; final List<Shadow>? shadows;
/// Linearly interpolate between two icon theme data objects. /// Linearly interpolate between two icon theme data objects.
...@@ -100,9 +169,13 @@ class IconThemeData with Diagnosticable { ...@@ -100,9 +169,13 @@ class IconThemeData with Diagnosticable {
static IconThemeData lerp(IconThemeData? a, IconThemeData? b, double t) { static IconThemeData lerp(IconThemeData? a, IconThemeData? b, double t) {
assert(t != null); assert(t != null);
return IconThemeData( return IconThemeData(
size: ui.lerpDouble(a?.size, b?.size, t),
fill: ui.lerpDouble(a?.fill, b?.fill, t),
weight: ui.lerpDouble(a?.weight, b?.weight, t),
grade: ui.lerpDouble(a?.grade, b?.grade, t),
opticalSize: ui.lerpDouble(a?.opticalSize, b?.opticalSize, t),
color: Color.lerp(a?.color, b?.color, t), color: Color.lerp(a?.color, b?.color, t),
opacity: ui.lerpDouble(a?.opacity, b?.opacity, t), opacity: ui.lerpDouble(a?.opacity, b?.opacity, t),
size: ui.lerpDouble(a?.size, b?.size, t),
shadows: Shadow.lerpList(a?.shadows, b?.shadows, t), shadows: Shadow.lerpList(a?.shadows, b?.shadows, t),
); );
} }
...@@ -113,26 +186,38 @@ class IconThemeData with Diagnosticable { ...@@ -113,26 +186,38 @@ class IconThemeData with Diagnosticable {
return false; return false;
} }
return other is IconThemeData return other is IconThemeData
&& other.size == size
&& other.fill == fill
&& other.weight == weight
&& other.grade == grade
&& other.opticalSize == opticalSize
&& other.color == color && other.color == color
&& other.opacity == opacity && other.opacity == opacity
&& other.size == size
&& listEquals(other.shadows, shadows); && listEquals(other.shadows, shadows);
} }
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
size,
fill,
weight,
grade,
opticalSize,
color, color,
opacity, opacity,
size,
shadows == null ? null : Object.hashAll(shadows!), shadows == null ? null : Object.hashAll(shadows!),
); );
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DoubleProperty('size', size, defaultValue: null));
properties.add(DoubleProperty('fill', fill, defaultValue: null));
properties.add(DoubleProperty('weight', weight, defaultValue: null));
properties.add(DoubleProperty('grade', grade, defaultValue: null));
properties.add(DoubleProperty('opticalSize', opticalSize, defaultValue: null));
properties.add(ColorProperty('color', color, defaultValue: null)); properties.add(ColorProperty('color', color, defaultValue: null));
properties.add(DoubleProperty('opacity', opacity, defaultValue: null)); properties.add(DoubleProperty('opacity', opacity, defaultValue: null));
properties.add(DoubleProperty('size', size, defaultValue: null));
properties.add(IterableProperty<Shadow>('shadows', shadows, defaultValue: null)); properties.add(IterableProperty<Shadow>('shadows', shadows, defaultValue: null));
} }
} }
...@@ -7,7 +7,15 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -7,7 +7,15 @@ import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
testWidgets('IconTheme.of works', (WidgetTester tester) async { testWidgets('IconTheme.of works', (WidgetTester tester) async {
const IconThemeData data = IconThemeData(color: Color(0xAAAAAAAA), opacity: 0.5, size: 16.0); const IconThemeData data = IconThemeData(
size: 16.0,
fill: 0.0,
weight: 400.0,
grade: 0.0,
opticalSize: 48.0,
color: Color(0xAAAAAAAA),
opacity: 0.5
);
late IconThemeData retrieved; late IconThemeData retrieved;
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// 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';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -207,4 +209,42 @@ void main() { ...@@ -207,4 +209,42 @@ void main() {
expect(const IconData(123).hashCode, isNot(const IconData(123, fontPackage: 'p').hashCode)); expect(const IconData(123).hashCode, isNot(const IconData(123, fontPackage: 'p').hashCode));
expect(const IconData(123).toString(), 'IconData(U+0007B)'); expect(const IconData(123).toString(), 'IconData(U+0007B)');
}); });
testWidgets('Fill, weight, grade, and optical size variations are passed', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Icon(Icons.abc),
),
);
RichText text = tester.widget(find.byType(RichText));
expect(text.text.style!.fontVariations, <FontVariation>[]);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Icon(Icons.abc, fill: 0.5, weight: 300, grade: 200, opticalSize: 48),
),
);
text = tester.widget(find.byType(RichText));
expect(text.text.style!.fontVariations, isNotNull);
expect(text.text.style!.fontVariations, <FontVariation>[
const FontVariation('FILL', 0.5),
const FontVariation('wght', 300.0),
const FontVariation('GRAD', 200.0),
const FontVariation('opsz', 48.0)
]);
});
test('Throws if given invalid values', () {
expect(() => Icon(Icons.abc, fill: -0.1), throwsAssertionError);
expect(() => Icon(Icons.abc, fill: 1.1), throwsAssertionError);
expect(() => Icon(Icons.abc, weight: -0.1), throwsAssertionError);
expect(() => Icon(Icons.abc, weight: 0.0), throwsAssertionError);
expect(() => Icon(Icons.abc, opticalSize: -0.1), throwsAssertionError);
expect(() => Icon(Icons.abc, opticalSize: 0), throwsAssertionError);
});
} }
...@@ -7,44 +7,89 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -7,44 +7,89 @@ import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
test('IconThemeData control test', () { test('IconThemeData control test', () {
const IconThemeData data = IconThemeData(color: Color(0xAAAAAAAA), opacity: 0.5, size: 16.0, shadows: <Shadow>[Shadow(color: Color(0xAAAAAAAA), blurRadius: 1.0, offset: Offset(1.0, 1.0))]); const IconThemeData data = IconThemeData(
size: 16.0,
fill: 0.5,
weight: 600,
grade: 25,
opticalSize: 45,
color: Color(0xAAAAAAAA),
opacity: 0.5,
shadows: <Shadow>[Shadow(color: Color(0xAAAAAAAA), blurRadius: 1.0, offset: Offset(1.0, 1.0))],
);
expect(data, hasOneLineDescription); expect(data, hasOneLineDescription);
expect(data, equals(data.copyWith())); expect(data, equals(data.copyWith()));
expect(data.hashCode, equals(data.copyWith().hashCode)); expect(data.hashCode, equals(data.copyWith().hashCode));
final IconThemeData lerped = IconThemeData.lerp(data, const IconThemeData.fallback(), 0.25); final IconThemeData lerped = IconThemeData.lerp(data, const IconThemeData.fallback(), 0.25);
expect(lerped.size, 18.0);
expect(lerped.fill, 0.375);
expect(lerped.weight, 550.0);
expect(lerped.grade, 18.75);
expect(lerped.opticalSize, 45.75);
expect(lerped.color, const Color(0xBF7F7F7F)); expect(lerped.color, const Color(0xBF7F7F7F));
expect(lerped.opacity, 0.625); expect(lerped.opacity, 0.625);
expect(lerped.size, 18.0);
expect(lerped.shadows, const <Shadow>[Shadow(color: Color(0xAAAAAAAA), blurRadius: 0.75, offset: Offset(0.75, 0.75))]); expect(lerped.shadows, const <Shadow>[Shadow(color: Color(0xAAAAAAAA), blurRadius: 0.75, offset: Offset(0.75, 0.75))]);
}); });
test('IconThemeData lerp with first null', () { group('IconThemeData lerp', () {
const IconThemeData data = IconThemeData(color: Color(0xFFFFFFFF), opacity: 1.0, size: 16.0, shadows: <Shadow>[Shadow(color: Color(0xFFFFFFFF), blurRadius: 1.0, offset: Offset(1.0, 1.0))]); const IconThemeData data = IconThemeData(
size: 16.0,
fill: 0.5,
weight: 600,
grade: 25,
opticalSize: 45,
color: Color(0xFFFFFFFF),
opacity: 1.0,
shadows: <Shadow>[Shadow(color: Color(0xFFFFFFFF), blurRadius: 1.0, offset: Offset(1.0, 1.0))],
);
final IconThemeData lerped = IconThemeData.lerp(null, data, 0.25); test('with first null', () {
expect(lerped.color, const Color(0x40FFFFFF)); final IconThemeData lerped = IconThemeData.lerp(null, data, 0.25);
expect(lerped.opacity, 0.25);
expect(lerped.size, 4.0); expect(lerped.size, 4.0);
expect(lerped.shadows, const <Shadow>[Shadow(color: Color(0xFFFFFFFF), blurRadius: 0.25, offset: Offset(0.25, 0.25))]); expect(lerped.fill, 0.125);
}); expect(lerped.weight, 150.0);
expect(lerped.grade, 6.25);
expect(lerped.opticalSize, 11.25);
expect(lerped.color, const Color(0x40FFFFFF));
expect(lerped.opacity, 0.25);
expect(lerped.shadows, const <Shadow>[Shadow(color: Color(0xFFFFFFFF), blurRadius: 0.25, offset: Offset(0.25, 0.25))]);
});
test('with second null', () {
final IconThemeData lerped = IconThemeData.lerp(data, null, 0.25);
expect(lerped.size, 12.0);
expect(lerped.fill, 0.375);
expect(lerped.weight, 450.0);
expect(lerped.grade, 18.75);
expect(lerped.opticalSize, 33.75);
expect(lerped.color, const Color(0xBFFFFFFF));
expect(lerped.opacity, 0.75);
expect(lerped.shadows, const <Shadow>[Shadow(color: Color(0xFFFFFFFF), blurRadius: 0.75, offset: Offset(0.75, 0.75))]);
});
test('IconThemeData lerp with second null', () { test('with both null', () {
const IconThemeData data = IconThemeData(color: Color(0xFFFFFFFF), opacity: 1.0, size: 16.0, shadows: <Shadow>[Shadow(color: Color(0xFFFFFFFF), blurRadius: 1.0, offset: Offset(1.0, 1.0))]); final IconThemeData lerped = IconThemeData.lerp(null, null, 0.25);
final IconThemeData lerped = IconThemeData.lerp(data, null, 0.25); expect(lerped.size, null);
expect(lerped.color, const Color(0xBFFFFFFF)); expect(lerped.fill, null);
expect(lerped.opacity, 0.75); expect(lerped.weight, null);
expect(lerped.size, 12.0); expect(lerped.grade, null);
expect(lerped.shadows, const <Shadow>[Shadow(color: Color(0xFFFFFFFF), blurRadius: 0.75, offset: Offset(0.75, 0.75))]); expect(lerped.opticalSize, null);
expect(lerped.color, null);
expect(lerped.opacity, null);
expect(lerped.shadows, null);
});
}); });
test('IconThemeData lerp with both null', () { test('Throws if given invalid values', () {
final IconThemeData lerped = IconThemeData.lerp(null, null, 0.25); expect(() => IconThemeData(fill: -0.1), throwsAssertionError);
expect(lerped.color, null); expect(() => IconThemeData(fill: 1.1), throwsAssertionError);
expect(lerped.opacity, null); expect(() => IconThemeData(weight: -0.1), throwsAssertionError);
expect(lerped.size, null); expect(() => IconThemeData(weight: 0.0), throwsAssertionError);
expect(lerped.shadows, null); expect(() => IconThemeData(opticalSize: -0.1), throwsAssertionError);
expect(() => IconThemeData(opticalSize: 0), throwsAssertionError);
}); });
} }
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