Unverified Commit f7b00234 authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

Added `IconButtonTheme` and apply it to `IconButton` in M3 (#108332)

* Created IconButtonTheme and apply it to IconButton
parent 7e8f0e57
......@@ -88,6 +88,10 @@ class _${blockName}DefaultsM3 extends ButtonStyle {
MaterialStateProperty<Size>? get maximumSize =>
ButtonStyleButton.allOrNull<Size>(Size.infinite);
@override
MaterialStateProperty<double>? get iconSize =>
ButtonStyleButton.allOrNull<double>(${tokens["md.comp.icon-button.icon.size"]});
// No default side
@override
......
......@@ -89,6 +89,7 @@ export 'src/material/flutter_logo.dart';
export 'src/material/grid_tile.dart';
export 'src/material/grid_tile_bar.dart';
export 'src/material/icon_button.dart';
export 'src/material/icon_button_theme.dart';
export 'src/material/icons.dart';
export 'src/material/ink_decoration.dart';
export 'src/material/ink_highlight.dart';
......
......@@ -4,6 +4,8 @@
import 'package:flutter/painting.dart';
import 'colors.dart';
/// The minimum dimension of any interactive region according to Material
/// guidelines.
///
......@@ -47,3 +49,13 @@ const EdgeInsets kTabLabelPadding = EdgeInsets.symmetric(horizontal: 16.0);
/// The padding added around material list items.
const EdgeInsets kMaterialListPadding = EdgeInsets.symmetric(vertical: 8.0);
/// The default color for [ThemeData.iconTheme] when [ThemeData.brightness] is
/// [Brightness.light]. This color is used in [IconButton] to detect whether
/// [IconTheme.of(context).color] is the same as the default color of [ThemeData.iconTheme].
const Color kDefaultIconLightColor = Colors.white;
/// The default color for [ThemeData.iconTheme] when [ThemeData.brightness] is
/// [Brightness.dark]. This color is used in [IconButton] to detect whether
/// [IconTheme.of(context).color] is the same as the default color of [ThemeData.iconTheme].
const Color kDefaultIconDarkColor = Colors.black87;
......@@ -14,6 +14,7 @@ import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'debug.dart';
import 'icon_button_theme.dart';
import 'icons.dart';
import 'ink_well.dart';
import 'material.dart';
......@@ -37,7 +38,9 @@ const double _kMinButtonSize = kMinInteractiveDimension;
/// If the [onPressed] callback is null, then the button will be disabled and
/// will not react to touch.
///
/// Requires one of its ancestors to be a [Material] widget.
/// Requires one of its ancestors to be a [Material] widget. In Material Design 3,
/// this requirement no longer exists because this widget builds a subclass of
/// [ButtonStyleButton].
///
/// The hit region of an icon button will, if possible, be at least
/// kMinInteractiveDimension pixels in size, regardless of the actual
......@@ -109,6 +112,12 @@ const double _kMinButtonSize = kMinInteractiveDimension;
/// null then it will behave as a toggle button. If [isSelected] is true then it will
/// show [selectedIcon], if it false it will show the normal [icon].
///
/// In Material Design 3, both [IconTheme] and [IconButtonTheme] are used to override the default style
/// of [IconButton]. If both themes exist, the [IconButtonTheme] will override [IconTheme] no matter
/// which is closer to the [IconButton]. Each [IconButton]'s property is resolved by the order of
/// precedence: widget property, [IconButtonTheme] property, [IconTheme] property and
/// internal default property value.
///
/// {@tool dartpad}
/// This sample shows creation of [IconButton] widgets for standard, filled,
/// filled tonal and outlined types, as described in: https://m3.material.io/components/icon-buttons/overview
......@@ -139,10 +148,10 @@ class IconButton extends StatelessWidget {
/// Icon buttons are commonly used in the [AppBar.actions] field, but they can
/// be used in many other places as well.
///
/// Requires one of its ancestors to be a [Material] widget.
/// Requires one of its ancestors to be a [Material] widget. This requirement
/// no longer exists if [ThemeData.useMaterial3] is set to true.
///
/// The [iconSize], [padding], [autofocus], and [alignment] arguments must not
/// be null (though they each have default values).
/// [autofocus] argument must not be null (though it has default value).
///
/// The [icon] argument must be specified, and is typically either an [Icon]
/// or an [ImageIcon].
......@@ -150,8 +159,8 @@ class IconButton extends StatelessWidget {
super.key,
this.iconSize,
this.visualDensity,
this.padding = const EdgeInsets.all(8.0),
this.alignment = Alignment.center,
this.padding,
this.alignment,
this.splashRadius,
this.color,
this.focusColor,
......@@ -164,15 +173,13 @@ class IconButton extends StatelessWidget {
this.focusNode,
this.autofocus = false,
this.tooltip,
this.enableFeedback = true,
this.enableFeedback,
this.constraints,
this.style,
this.isSelected,
this.selectedIcon,
required this.icon,
}) : assert(padding != null),
assert(alignment != null),
assert(splashRadius == null || splashRadius > 0),
}) : assert(splashRadius == null || splashRadius > 0),
assert(autofocus != null),
assert(icon != null);
......@@ -187,6 +194,10 @@ class IconButton extends StatelessWidget {
/// fit the [Icon]. If you were to set the size of the [Icon] using
/// [Icon.size] instead, then the [IconButton] would default to 24.0 and then
/// the [Icon] itself would likely get clipped.
///
/// If [ThemeData.useMaterial3] is set to true and this is null, the size of the
/// [IconButton] would default to 24.0. The size given here is passed down to the
/// [ButtonStyle.iconSize] property.
final double? iconSize;
/// Defines how compact the icon button's layout will be.
......@@ -202,12 +213,12 @@ class IconButton extends StatelessWidget {
/// The padding around the button's icon. The entire padded icon will react
/// to input gestures.
///
/// This property must not be null. It defaults to 8.0 padding on all sides.
final EdgeInsetsGeometry padding;
/// This property can be null. If null, it defaults to 8.0 padding on all sides.
final EdgeInsetsGeometry? padding;
/// Defines how the icon is positioned within the IconButton.
///
/// This property must not be null. It defaults to [Alignment.center].
/// This property can be null. If null, it defaults to [Alignment.center].
///
/// See also:
///
......@@ -215,7 +226,7 @@ class IconButton extends StatelessWidget {
/// specify an [AlignmentGeometry].
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
/// relative to text direction.
final AlignmentGeometry alignment;
final AlignmentGeometry? alignment;
/// The splash radius.
///
......@@ -353,7 +364,7 @@ class IconButton extends StatelessWidget {
/// See also:
///
/// * [Feedback] for providing platform-specific feedback to certain actions.
final bool enableFeedback;
final bool? enableFeedback;
/// Optional size constraints for the button.
///
......@@ -465,6 +476,7 @@ class IconButton extends StatelessWidget {
Size? minimumSize,
Size? fixedSize,
Size? maximumSize,
double? iconSize,
BorderSide? side,
OutlinedBorder? shape,
EdgeInsetsGeometry? padding,
......@@ -501,6 +513,7 @@ class IconButton extends StatelessWidget {
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
iconSize: ButtonStyleButton.allOrNull<double>(iconSize),
side: ButtonStyleButton.allOrNull<BorderSide>(side),
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
mouseCursor: mouseCursor,
......@@ -516,25 +529,6 @@ class IconButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
if (!theme.useMaterial3) {
assert(debugCheckHasMaterial(context));
}
Color? currentColor;
if (onPressed != null) {
currentColor = color;
} else {
currentColor = disabledColor ?? theme.disabledColor;
}
final VisualDensity effectiveVisualDensity = visualDensity ?? theme.visualDensity;
final BoxConstraints unadjustedConstraints = constraints ?? const BoxConstraints(
minWidth: _kMinButtonSize,
minHeight: _kMinButtonSize,
);
final BoxConstraints adjustedConstraints = effectiveVisualDensity.effectiveConstraints(unadjustedConstraints);
final double effectiveIconSize = iconSize ?? IconTheme.of(context).size ?? 24.0;
if (theme.useMaterial3) {
final Size? minSize = constraints == null
......@@ -554,6 +548,7 @@ class IconButton extends StatelessWidget {
padding: padding,
minimumSize: minSize,
maximumSize: maxSize,
iconSize: iconSize,
alignment: alignment,
enabledMouseCursor: mouseCursor,
disabledMouseCursor: mouseCursor,
......@@ -568,16 +563,11 @@ class IconButton extends StatelessWidget {
effectiveIcon = selectedIcon!;
}
Widget iconButton = IconTheme.merge(
data: IconThemeData(
size: effectiveIconSize,
),
child: effectiveIcon,
);
Widget iconButton = effectiveIcon;
if (tooltip != null) {
iconButton = Tooltip(
message: tooltip,
child: iconButton,
child: effectiveIcon,
);
}
......@@ -591,15 +581,36 @@ class IconButton extends StatelessWidget {
);
}
assert(debugCheckHasMaterial(context));
Color? currentColor;
if (onPressed != null) {
currentColor = color;
} else {
currentColor = disabledColor ?? theme.disabledColor;
}
final VisualDensity effectiveVisualDensity = visualDensity ?? theme.visualDensity;
final BoxConstraints unadjustedConstraints = constraints ?? const BoxConstraints(
minWidth: _kMinButtonSize,
minHeight: _kMinButtonSize,
);
final BoxConstraints adjustedConstraints = effectiveVisualDensity.effectiveConstraints(unadjustedConstraints);
final double effectiveIconSize = iconSize ?? IconTheme.of(context).size ?? 24.0;
final EdgeInsetsGeometry effectivePadding = padding ?? const EdgeInsets.all(8.0);
final AlignmentGeometry effectiveAlignment = alignment ?? Alignment.center;
final bool effectiveEnableFeedback = enableFeedback ?? true;
Widget result = ConstrainedBox(
constraints: adjustedConstraints,
child: Padding(
padding: padding,
padding: effectivePadding,
child: SizedBox(
height: effectiveIconSize,
width: effectiveIconSize,
child: Align(
alignment: alignment,
alignment: effectiveAlignment,
child: IconTheme.merge(
data: IconThemeData(
size: effectiveIconSize,
......@@ -628,14 +639,14 @@ class IconButton extends StatelessWidget {
canRequestFocus: onPressed != null,
onTap: onPressed,
mouseCursor: mouseCursor ?? (onPressed == null ? SystemMouseCursors.basic : SystemMouseCursors.click),
enableFeedback: enableFeedback,
enableFeedback: effectiveEnableFeedback,
focusColor: focusColor ?? theme.focusColor,
hoverColor: hoverColor ?? theme.hoverColor,
highlightColor: highlightColor ?? theme.highlightColor,
splashColor: splashColor ?? theme.splashColor,
radius: splashRadius ?? math.max(
Material.defaultSplashRadius,
(effectiveIconSize + math.min(padding.horizontal, padding.vertical)) * 0.7,
(effectiveIconSize + math.min(effectivePadding.horizontal, effectivePadding.vertical)) * 0.7,
// x 0.5 for diameter -> radius and + 40% overflow derived from other Material apps.
),
child: result,
......@@ -762,6 +773,7 @@ class _IconButtonM3 extends ButtonStyleButton {
/// * `minimumSize` - Size(40, 40)
/// * `fixedSize` - null
/// * `maximumSize` - Size.infinite
/// * `iconSize` - 24
/// * `side` - null
/// * `shape` - StadiumBorder()
/// * `mouseCursor`
......@@ -778,10 +790,30 @@ class _IconButtonM3 extends ButtonStyleButton {
return _IconButtonDefaultsM3(context);
}
/// Returns null because [IconButton] doesn't have its component theme.
/// Returns the [IconButtonThemeData.style] of the closest [IconButtonTheme] ancestor.
/// The color and icon size can also be configured by the [IconTheme] if the same property
/// has a null value in [IconButtonTheme]. However, if any of the properties exist
/// in both [IconButtonTheme] and [IconTheme], [IconTheme] will be overridden.
@override
ButtonStyle? themeStyleOf(BuildContext context) {
return null;
final IconThemeData iconTheme = IconTheme.of(context);
final bool isDark = Theme.of(context).brightness == Brightness.dark;
bool isIconThemeDefault(Color? color) {
if (isDark) {
return color == kDefaultIconLightColor;
}
return color == kDefaultIconDarkColor;
}
final bool isDefaultColor = isIconThemeDefault(iconTheme.color);
final bool isDefaultSize = iconTheme.size == const IconThemeData.fallback().size;
final ButtonStyle iconThemeStyle = IconButton.styleFrom(
foregroundColor: isDefaultColor ? null : iconTheme.color,
iconSize: isDefaultSize ? null : iconTheme.size
);
return IconButtonTheme.of(context).style?.merge(iconThemeStyle) ?? iconThemeStyle;
}
}
......@@ -969,6 +1001,10 @@ class _IconButtonDefaultsM3 extends ButtonStyle {
MaterialStateProperty<Size>? get maximumSize =>
ButtonStyleButton.allOrNull<Size>(Size.infinite);
@override
MaterialStateProperty<double>? get iconSize =>
ButtonStyleButton.allOrNull<double>(24.0);
// No default side
@override
......
// 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 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'theme.dart';
/// A [ButtonStyle] that overrides the default appearance of
/// [IconButton]s when it's used with the [IconButton], the [IconButtonTheme] or the
/// overall [Theme]'s [ThemeData.iconButtonTheme].
///
/// The [IconButton] will be affected by [IconButtonTheme] and [IconButtonThemeData]
/// only if [ThemeData.useMaterial3] is set to true; otherwise, [IconTheme] will be used.
///
/// The [style]'s properties override [IconButton]'s default style. Only
/// the style's non-null property values or resolved non-null
/// [MaterialStateProperty] values are used.
///
/// See also:
///
/// * [IconButtonTheme], the theme which is configured with this class.
/// * [IconButton.styleFrom], which converts simple values into a
/// [ButtonStyle] that's consistent with [IconButton]'s defaults.
/// * [MaterialStateProperty.resolve], "resolve" a material state property
/// to a simple value based on a set of [MaterialState]s.
/// * [ThemeData.iconButtonTheme], which can be used to override the default
/// [ButtonStyle] for [IconButton]s below the overall [Theme].
@immutable
class IconButtonThemeData with Diagnosticable {
/// Creates a [IconButtonThemeData].
///
/// The [style] may be null.
const IconButtonThemeData({ this.style });
/// Overrides for [IconButton]'s default style if [ThemeData.useMaterial3]
/// is set to true.
///
/// Non-null properties or non-null resolved [MaterialStateProperty]
/// values override the default [ButtonStyle] in [IconButton].
///
/// If [style] is null, then this theme doesn't override anything.
final ButtonStyle? style;
/// Linearly interpolate between two icon button themes.
static IconButtonThemeData? lerp(IconButtonThemeData? a, IconButtonThemeData? b, double t) {
assert (t != null);
if (a == null && b == null) {
return null;
}
return IconButtonThemeData(
style: ButtonStyle.lerp(a?.style, b?.style, t),
);
}
@override
int get hashCode => style.hashCode;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is IconButtonThemeData && other.style == style;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null));
}
}
/// Overrides the default [ButtonStyle] of its [IconButton] descendants.
///
/// See also:
///
/// * [IconButtonThemeData], which is used to configure this theme.
/// * [IconButton.styleFrom], which converts simple values into a
/// [ButtonStyle] that's consistent with [IconButton]'s defaults.
/// * [ThemeData.iconButtonTheme], which can be used to override the default
/// [ButtonStyle] for [IconButton]s below the overall [Theme].
class IconButtonTheme extends InheritedTheme {
/// Create a [IconButtonTheme].
///
/// The [data] parameter must not be null.
const IconButtonTheme({
super.key,
required this.data,
required super.child,
}) : assert(data != null);
/// The configuration of this theme.
final IconButtonThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [IconButtonTheme] widget, then
/// [ThemeData.iconButtonTheme] is used.
///
/// Typical usage is as follows:
///
/// ```dart
/// IconButtonThemeData theme = IconButtonTheme.of(context);
/// ```
static IconButtonThemeData of(BuildContext context) {
final IconButtonTheme? buttonTheme = context.dependOnInheritedWidgetOfExactType<IconButtonTheme>();
return buttonTheme?.data ?? Theme.of(context).iconButtonTheme;
}
@override
Widget wrap(BuildContext context, Widget child) {
return IconButtonTheme(data: data, child: child);
}
@override
bool updateShouldNotify(IconButtonTheme oldWidget) => data != oldWidget.data;
}
......@@ -37,7 +37,6 @@ const double _kMenuMinWidth = 2.0 * _kMenuWidthStep;
const double _kMenuVerticalPadding = 8.0;
const double _kMenuWidthStep = 56.0;
const double _kMenuScreenPadding = 8.0;
const double _kDefaultIconSize = 24.0;
/// Used to configure how the [PopupMenuButton] positions its popup menu.
enum PopupMenuPosition {
......@@ -1241,7 +1240,6 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
@override
Widget build(BuildContext context) {
final IconThemeData iconTheme = IconTheme.of(context);
final bool enableFeedback = widget.enableFeedback
?? PopupMenuTheme.of(context).enableFeedback
?? true;
......@@ -1265,7 +1263,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
icon: widget.icon ?? Icon(Icons.adaptive.more),
padding: widget.padding,
splashRadius: widget.splashRadius,
iconSize: widget.iconSize ?? iconTheme.size ?? _kDefaultIconSize,
iconSize: widget.iconSize,
tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
onPressed: widget.enabled ? showButtonMenu : null,
enableFeedback: enableFeedback,
......
......@@ -19,6 +19,7 @@ import 'checkbox_theme.dart';
import 'chip_theme.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'data_table_theme.dart';
import 'dialog_theme.dart';
import 'divider_theme.dart';
......@@ -26,6 +27,7 @@ import 'drawer_theme.dart';
import 'elevated_button_theme.dart';
import 'expansion_tile_theme.dart';
import 'floating_action_button_theme.dart';
import 'icon_button_theme.dart';
import 'ink_ripple.dart';
import 'ink_sparkle.dart';
import 'ink_splash.dart';
......@@ -114,6 +116,7 @@ const Color _kDarkThemeSplashColor = Color(0x40CCCCCC);
/// * [OutlinedButton]
/// * [TextButton]
/// * [ElevatedButton]
/// * [IconButton]
/// * The time picker widget ([showTimePicker])
/// * [SnackBar]
/// * [Chip]
......@@ -339,6 +342,7 @@ class ThemeData with Diagnosticable {
ElevatedButtonThemeData? elevatedButtonTheme,
ExpansionTileThemeData? expansionTileTheme,
FloatingActionButtonThemeData? floatingActionButtonTheme,
IconButtonThemeData? iconButtonTheme,
ListTileThemeData? listTileTheme,
NavigationBarThemeData? navigationBarTheme,
NavigationRailThemeData? navigationRailTheme,
......@@ -535,7 +539,7 @@ class ThemeData with Diagnosticable {
}
textTheme = defaultTextTheme.merge(textTheme);
primaryTextTheme = defaultPrimaryTextTheme.merge(primaryTextTheme);
iconTheme ??= isDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black87);
iconTheme ??= isDark ? const IconThemeData(color: kDefaultIconLightColor) : const IconThemeData(color: kDefaultIconDarkColor);
primaryIconTheme ??= primaryIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
// COMPONENT THEMES
......@@ -554,6 +558,7 @@ class ThemeData with Diagnosticable {
drawerTheme ??= const DrawerThemeData();
elevatedButtonTheme ??= const ElevatedButtonThemeData();
floatingActionButtonTheme ??= const FloatingActionButtonThemeData();
iconButtonTheme ??= const IconButtonThemeData();
listTileTheme ??= const ListTileThemeData();
navigationBarTheme ??= const NavigationBarThemeData();
navigationRailTheme ??= const NavigationRailThemeData();
......@@ -645,6 +650,7 @@ class ThemeData with Diagnosticable {
elevatedButtonTheme: elevatedButtonTheme,
expansionTileTheme: expansionTileTheme,
floatingActionButtonTheme: floatingActionButtonTheme,
iconButtonTheme: iconButtonTheme,
listTileTheme: listTileTheme,
navigationBarTheme: navigationBarTheme,
navigationRailTheme: navigationRailTheme,
......@@ -750,6 +756,7 @@ class ThemeData with Diagnosticable {
required this.elevatedButtonTheme,
required this.expansionTileTheme,
required this.floatingActionButtonTheme,
required this.iconButtonTheme,
required this.listTileTheme,
required this.navigationBarTheme,
required this.navigationRailTheme,
......@@ -892,6 +899,7 @@ class ThemeData with Diagnosticable {
assert(elevatedButtonTheme != null),
assert(expansionTileTheme != null),
assert(floatingActionButtonTheme != null),
assert(iconButtonTheme != null),
assert(listTileTheme != null),
assert(navigationBarTheme != null),
assert(navigationRailTheme != null),
......@@ -1444,6 +1452,10 @@ class ThemeData with Diagnosticable {
/// [FloatingActionButton].
final FloatingActionButtonThemeData floatingActionButtonTheme;
/// A theme for customizing the appearance and internal layout of
/// [IconButton]s.
final IconButtonThemeData iconButtonTheme;
/// A theme for customizing the appearance of [ListTile] widgets.
final ListTileThemeData listTileTheme;
......@@ -1712,6 +1724,7 @@ class ThemeData with Diagnosticable {
ElevatedButtonThemeData? elevatedButtonTheme,
ExpansionTileThemeData? expansionTileTheme,
FloatingActionButtonThemeData? floatingActionButtonTheme,
IconButtonThemeData? iconButtonTheme,
ListTileThemeData? listTileTheme,
NavigationBarThemeData? navigationBarTheme,
NavigationRailThemeData? navigationRailTheme,
......@@ -1852,6 +1865,7 @@ class ThemeData with Diagnosticable {
elevatedButtonTheme: elevatedButtonTheme ?? this.elevatedButtonTheme,
expansionTileTheme: expansionTileTheme ?? this.expansionTileTheme,
floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme,
iconButtonTheme: iconButtonTheme ?? this.iconButtonTheme,
listTileTheme: listTileTheme ?? this.listTileTheme,
navigationBarTheme: navigationBarTheme ?? this.navigationBarTheme,
navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme,
......@@ -2050,6 +2064,7 @@ class ThemeData with Diagnosticable {
elevatedButtonTheme: ElevatedButtonThemeData.lerp(a.elevatedButtonTheme, b.elevatedButtonTheme, t)!,
expansionTileTheme: ExpansionTileThemeData.lerp(a.expansionTileTheme, b.expansionTileTheme, t)!,
floatingActionButtonTheme: FloatingActionButtonThemeData.lerp(a.floatingActionButtonTheme, b.floatingActionButtonTheme, t)!,
iconButtonTheme: IconButtonThemeData.lerp(a.iconButtonTheme, b.iconButtonTheme, t)!,
listTileTheme: ListTileThemeData.lerp(a.listTileTheme, b.listTileTheme, t)!,
navigationBarTheme: NavigationBarThemeData.lerp(a.navigationBarTheme, b.navigationBarTheme, t)!,
navigationRailTheme: NavigationRailThemeData.lerp(a.navigationRailTheme, b.navigationRailTheme, t)!,
......@@ -2150,6 +2165,7 @@ class ThemeData with Diagnosticable {
other.elevatedButtonTheme == elevatedButtonTheme &&
other.expansionTileTheme == expansionTileTheme &&
other.floatingActionButtonTheme == floatingActionButtonTheme &&
other.iconButtonTheme == iconButtonTheme &&
other.listTileTheme == listTileTheme &&
other.navigationBarTheme == navigationBarTheme &&
other.navigationRailTheme == navigationRailTheme &&
......@@ -2247,6 +2263,7 @@ class ThemeData with Diagnosticable {
elevatedButtonTheme,
expansionTileTheme,
floatingActionButtonTheme,
iconButtonTheme,
listTileTheme,
navigationBarTheme,
navigationRailTheme,
......@@ -2346,6 +2363,7 @@ class ThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<ElevatedButtonThemeData>('elevatedButtonTheme', elevatedButtonTheme, defaultValue: defaultData.elevatedButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<ExpansionTileThemeData>('expansionTileTheme', expansionTileTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<FloatingActionButtonThemeData>('floatingActionButtonTheme', floatingActionButtonTheme, defaultValue: defaultData.floatingActionButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<IconButtonThemeData>('iconButtonTheme', iconButtonTheme, defaultValue: defaultData.iconButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<ListTileThemeData>('listTileTheme', listTileTheme, defaultValue: defaultData.listTileTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NavigationBarThemeData>('navigationBarTheme', navigationBarTheme, defaultValue: defaultData.navigationBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NavigationRailThemeData>('navigationRailTheme', navigationRailTheme, defaultValue: defaultData.navigationRailTheme, level: DiagnosticLevel.debug));
......
......@@ -54,6 +54,13 @@ ScrollController primaryScrollController(WidgetTester tester) {
return PrimaryScrollController.of(tester.element(find.byType(CustomScrollView)))!;
}
TextStyle? iconStyle(WidgetTester tester, IconData icon) {
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(icon).first, matching: find.byType(RichText)),
);
return iconRichText.text.style;
}
double appBarHeight(WidgetTester tester) => tester.getSize(find.byType(AppBar, skipOffstage: false)).height;
double appBarTop(WidgetTester tester) => tester.getTopLeft(find.byType(AppBar, skipOffstage: false)).dy;
double appBarBottom(WidgetTester tester) => tester.getBottomLeft(find.byType(AppBar, skipOffstage: false)).dy;
......@@ -544,6 +551,28 @@ void main() {
);
});
testWidgets('AppBar drawer icon has default color', (WidgetTester tester) async {
final ThemeData themeData = ThemeData.from(colorScheme: const ColorScheme.light());
final bool useMaterial3 = themeData.useMaterial3;
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
title: const Text('Howdy!'),
),
drawer: const Drawer(),
),
),
);
Color? iconColor() => iconStyle(tester, Icons.menu)?.color;
final Color iconColorM2 = themeData.colorScheme.onPrimary;
final Color iconColorM3 = themeData.colorScheme.onSurfaceVariant;
expect(iconColor(), useMaterial3 ? iconColorM3 : iconColorM2);
});
testWidgets('AppBar drawer icon is sized by iconTheme', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
......@@ -562,6 +591,28 @@ void main() {
);
});
testWidgets('AppBar drawer icon is colored by iconTheme', (WidgetTester tester) async {
final ThemeData themeData = ThemeData.from(colorScheme: const ColorScheme.light());
const Color color = Color(0xFF2196F3);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
title: const Text('Howdy!'),
iconTheme: const IconThemeData(color: color),
),
drawer: const Drawer(),
),
),
);
Color? iconColor() => iconStyle(tester, Icons.menu)?.color;
expect(iconColor(), color);
});
testWidgets('AppBar endDrawer icon has default size', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
......@@ -580,6 +631,28 @@ void main() {
);
});
testWidgets('AppBar endDrawer icon has default color', (WidgetTester tester) async {
final ThemeData themeData = ThemeData.from(colorScheme: const ColorScheme.light());
final bool useMaterial3 = themeData.useMaterial3;
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
title: const Text('Howdy!'),
),
endDrawer: const Drawer(),
),
),
);
Color? iconColor() => iconStyle(tester, Icons.menu)?.color;
final Color iconColorM2 = themeData.colorScheme.onPrimary;
final Color iconColorM3 = themeData.colorScheme.onSurfaceVariant;
expect(iconColor(), useMaterial3 ? iconColorM3 : iconColorM2);
});
testWidgets('AppBar endDrawer icon is sized by iconTheme', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
......@@ -598,6 +671,28 @@ void main() {
);
});
testWidgets('AppBar endDrawer icon is colored by iconTheme', (WidgetTester tester) async {
final ThemeData themeData = ThemeData.from(colorScheme: const ColorScheme.light());
const Color color = Color(0xFF2196F3);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
title: const Text('Howdy!'),
iconTheme: const IconThemeData(color: color),
),
endDrawer: const Drawer(),
),
),
);
Color? iconColor() => iconStyle(tester, Icons.menu)?.color;
expect(iconColor(), color);
});
testWidgets('leading button extends to edge and is square', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
......@@ -2744,7 +2839,7 @@ void main() {
backgroundColor: backgroundColor,
leading: Icon(Icons.add_circle, key: leadingIconKey),
title: const Text('title'),
actions: <Widget>[Icon(Icons.add_circle, key: actionIconKey), const Text('action')],
actions: <Widget>[Icon(Icons.ac_unit, key: actionIconKey), const Text('action')],
),
),
),
......@@ -2772,8 +2867,123 @@ void main() {
find.ancestor(of: find.byKey(actionIconKey), matching: find.byType(IconTheme)).first,
).data;
expect(actionIconTheme.color, foregroundColor);
// Test icon color
Color? leadingIconColor() => iconStyle(tester, Icons.add_circle)?.color;
Color? actionIconColor() => iconStyle(tester, Icons.ac_unit)?.color;
expect(leadingIconColor(), foregroundColor);
expect(actionIconColor(), foregroundColor);
});
// Regression test for https://github.com/flutter/flutter/issues/107305
group('Icons are colored correctly by IconTheme and ActionIconTheme in M3', () {
testWidgets('Icons and IconButtons are colored by IconTheme in M3', (WidgetTester tester) async {
const Color iconColor = Color(0xff00ff00);
final Key leadingIconKey = UniqueKey();
final Key actionIconKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(
colorScheme: const ColorScheme.light(), useMaterial3: true),
home: Scaffold(
appBar: AppBar(
iconTheme: const IconThemeData(color: iconColor),
leading: Icon(Icons.add_circle, key: leadingIconKey),
title: const Text('title'),
actions: <Widget>[
Icon(Icons.ac_unit, key: actionIconKey),
IconButton(icon: const Icon(Icons.add), onPressed: () {},)
],
),
),
),
);
Color? leadingIconColor() => iconStyle(tester, Icons.add_circle)?.color;
Color? actionIconColor() => iconStyle(tester, Icons.ac_unit)?.color;
Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color;
expect(leadingIconColor(), iconColor);
expect(actionIconColor(), iconColor);
expect(actionIconButtonColor(), iconColor);
});
testWidgets('Action icons and IconButtons are colored by ActionIconTheme - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData.from(
colorScheme: const ColorScheme.light(),
useMaterial3: true,
);
const Color actionsIconColor = Color(0xff0000ff);
final Key leadingIconKey = UniqueKey();
final Key actionIconKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
actionsIconTheme: const IconThemeData(color: actionsIconColor),
leading: Icon(Icons.add_circle, key: leadingIconKey),
title: const Text('title'),
actions: <Widget>[
Icon(Icons.ac_unit, key: actionIconKey),
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
],
),
),
),
);
Color? leadingIconColor() => iconStyle(tester, Icons.add_circle)?.color;
Color? actionIconColor() => iconStyle(tester, Icons.ac_unit)?.color;
Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color;
expect(leadingIconColor(), themeData.colorScheme.onSurface);
expect(actionIconColor(), actionsIconColor);
expect(actionIconButtonColor(), actionsIconColor);
});
testWidgets('The actionIconTheme property overrides iconTheme - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData.from(
colorScheme: const ColorScheme.light(),
useMaterial3: true,
);
const Color overallIconColor = Color(0xff00ff00);
const Color actionsIconColor = Color(0xff0000ff);
final Key leadingIconKey = UniqueKey();
final Key actionIconKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
iconTheme: const IconThemeData(color: overallIconColor),
actionsIconTheme: const IconThemeData(color: actionsIconColor),
leading: Icon(Icons.add_circle, key: leadingIconKey),
title: const Text('title'),
actions: <Widget>[
Icon(Icons.ac_unit, key: actionIconKey),
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
],
),
),
),
);
Color? leadingIconColor() => iconStyle(tester, Icons.add_circle)?.color;
Color? actionIconColor() => iconStyle(tester, Icons.ac_unit)?.color;
Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color;
expect(leadingIconColor(), overallIconColor);
expect(actionIconColor(), actionsIconColor);
expect(actionIconButtonColor(), actionsIconColor);
});
});
testWidgets('AppBarTheme.backwardsCompatibility', (WidgetTester tester) async {
const Color foregroundColor = Color(0xff00ff00);
......@@ -2793,7 +3003,7 @@ void main() {
foregroundColor: foregroundColor, // only applies if backwardsCompatibility is false
leading: Icon(Icons.add_circle, key: leadingIconKey),
title: const Text('title'),
actions: <Widget>[Icon(Icons.add_circle, key: actionIconKey), const Text('action')],
actions: <Widget>[Icon(Icons.ac_unit, key: actionIconKey), const Text('action')],
),
),
),
......@@ -2813,6 +3023,13 @@ void main() {
find.ancestor(of: find.byKey(actionIconKey), matching: find.byType(IconTheme)).first,
).data;
expect(actionIconTheme.color, foregroundColor);
// Test icon color
Color? leadingIconColor() => iconStyle(tester, Icons.add_circle)?.color;
Color? actionIconColor() => iconStyle(tester, Icons.ac_unit)?.color;
expect(leadingIconColor(), foregroundColor);
expect(actionIconColor(), foregroundColor);
});
group('MaterialStateColor scrolledUnder', () {
......
......@@ -376,6 +376,22 @@ void main() {
expect(box.size, const Size(96.0, 96.0));
});
testWidgets('test default alignment', (WidgetTester tester) async {
await tester.pumpWidget(
wrap(
useMaterial3: theme.useMaterial3,
child: IconButton(
onPressed: mockOnPressedFunction.handler,
icon: const Icon(Icons.ac_unit),
iconSize: 80.0,
),
),
);
final Align align = tester.firstWidget<Align>(find.ancestor(of: find.byIcon(Icons.ac_unit), matching: find.byType(Align)));
expect(align.alignment, Alignment.center);
});
testWidgets('test tooltip', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
......@@ -977,6 +993,48 @@ void main() {
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.none);
});
testWidgets('IconTheme opacity test', (WidgetTester tester) async {
final ThemeData theme = ThemeData.from(colorScheme: colorScheme, useMaterial3: false);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Scaffold(
body: Center(
child: IconButton(
icon: const Icon(Icons.add),
color: Colors.purple,
onPressed: () {},
)
),
),
)
);
Color? iconColor() => _iconStyle(tester, Icons.add)?.color;
expect(iconColor(), Colors.purple);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Scaffold(
body: Center(
child: IconTheme.merge(
data: const IconThemeData(opacity: 0.5),
child: IconButton(
icon: const Icon(Icons.add),
color: Colors.purple,
onPressed: () {},
),
)
),
),
)
);
Color? iconColorWithOpacity() => _iconStyle(tester, Icons.add)?.color;
expect(iconColorWithOpacity(), Colors.purple.withOpacity(0.5));
});
testWidgets('IconButton defaults - M3', (WidgetTester tester) async {
final ThemeData themeM3 = ThemeData.from(colorScheme: colorScheme, useMaterial3: true);
......@@ -1013,6 +1071,7 @@ void main() {
final Align align = tester.firstWidget<Align>(find.ancestor(of: find.byIcon(Icons.ac_unit), matching: find.byType(Align)));
expect(align.alignment, Alignment.center);
expect(tester.getSize(find.byIcon(Icons.ac_unit)), const Size(24.0, 24.0));
final Offset center = tester.getCenter(find.byType(IconButton));
final TestGesture gesture = await tester.startGesture(center);
......@@ -1569,6 +1628,194 @@ void main() {
expect(find.byIcon(Icons.account_box), findsNothing);
expect(find.byIcon(Icons.ac_unit), findsOneWidget);
});
group('IconTheme tests in Material 3', () {
testWidgets('IconTheme overrides default values in M3', (WidgetTester tester) async {
// Theme's IconTheme
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(
colorScheme: const ColorScheme.light(),
useMaterial3: true,
).copyWith(
iconTheme: const IconThemeData(color: Colors.red, size: 37),
),
home: IconButton(
icon: const Icon(Icons.account_box),
onPressed: () {},
)
)
);
Color? iconColor0() => _iconStyle(tester, Icons.account_box)?.color;
expect(iconColor0(), Colors.red);
expect(tester.getSize(find.byIcon(Icons.account_box)), equals(const Size(37, 37)),);
// custom IconTheme outside of IconButton
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(
colorScheme: const ColorScheme.light(),
useMaterial3: true,
),
home: IconTheme.merge(
data: const IconThemeData(color: Colors.pink, size: 35),
child: IconButton(
icon: const Icon(Icons.account_box),
onPressed: () {},
),
)
)
);
Color? iconColor1() => _iconStyle(tester, Icons.account_box)?.color;
expect(iconColor1(), Colors.pink);
expect(tester.getSize(find.byIcon(Icons.account_box)), equals(const Size(35, 35)),);
});
testWidgets('Theme IconButtonTheme overrides IconTheme in Material3', (WidgetTester tester) async {
// When IconButtonTheme and IconTheme both exist in ThemeData, the IconButtonTheme can override IconTheme.
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(
colorScheme: const ColorScheme.light(),
useMaterial3: true,
).copyWith(
iconTheme: const IconThemeData(color: Colors.red, size: 25),
iconButtonTheme: IconButtonThemeData(style: IconButton.styleFrom(foregroundColor: Colors.green, iconSize: 27),)
),
home: IconButton(
icon: const Icon(Icons.account_box),
onPressed: () {},
)
)
);
Color? iconColor() => _iconStyle(tester, Icons.account_box)?.color;
expect(iconColor(), Colors.green);
expect(tester.getSize(find.byIcon(Icons.account_box)), equals(const Size(27, 27)),);
});
testWidgets('Button IconButtonTheme always overrides IconTheme in Material3', (WidgetTester tester) async {
// When IconButtonTheme is closer to IconButton, IconButtonTheme overrides IconTheme
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(
colorScheme: const ColorScheme.light(),
useMaterial3: true,
),
home: IconTheme.merge(
data: const IconThemeData(color: Colors.orange, size: 36),
child: IconButtonTheme(
data: IconButtonThemeData(style: IconButton.styleFrom(foregroundColor: Colors.blue, iconSize: 35)),
child: IconButton(
icon: const Icon(Icons.account_box),
onPressed: () {},
),
),
)
)
);
Color? iconColor0() => _iconStyle(tester, Icons.account_box)?.color;
expect(iconColor0(), Colors.blue);
expect(tester.getSize(find.byIcon(Icons.account_box)), equals(const Size(35, 35)),);
// When IconTheme is closer to IconButton, IconButtonTheme still overrides IconTheme
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(
colorScheme: const ColorScheme.light(),
useMaterial3: true,
),
home: IconTheme.merge(
data: const IconThemeData(color: Colors.blue, size: 35),
child: IconButtonTheme(
data: IconButtonThemeData(style: IconButton.styleFrom(foregroundColor: Colors.orange, iconSize: 36)),
child: IconButton(
icon: const Icon(Icons.account_box),
onPressed: () {},
),
),
)
)
);
Color? iconColor1() => _iconStyle(tester, Icons.account_box)?.color;
expect(iconColor1(), Colors.orange);
expect(tester.getSize(find.byIcon(Icons.account_box)), equals(const Size(36, 36)),);
});
testWidgets('White icon color defined by users shows correctly in Material3', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(
colorScheme: const ColorScheme.dark(),
useMaterial3: true,
).copyWith(
iconTheme: const IconThemeData(color: Colors.white),
),
home: IconButton(
icon: const Icon(Icons.account_box),
onPressed: () {},
)
)
);
Color? iconColor1() => _iconStyle(tester, Icons.account_box)?.color;
expect(iconColor1(), Colors.white);
});
testWidgets('In light mode, icon color is M3 default color instead of IconTheme.of(context).color, '
'if only setting color in IconTheme', (WidgetTester tester) async {
final ColorScheme darkScheme = const ColorScheme.dark().copyWith(onSurfaceVariant: const Color(0xffe91e60));
// Brightness.dark
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(colorScheme: darkScheme, useMaterial3: true,),
home: Scaffold(
body: IconTheme.merge(
data: const IconThemeData(size: 26),
child: IconButton(
icon: const Icon(Icons.account_box),
onPressed: () {},
),
),
)
)
);
Color? iconColor0() => _iconStyle(tester, Icons.account_box)?.color;
expect(iconColor0(), darkScheme.onSurfaceVariant); // onSurfaceVariant
});
testWidgets('In dark mode, icon color is M3 default color instead of IconTheme.of(context).color, '
'if only setting color in IconTheme', (WidgetTester tester) async {
final ColorScheme lightScheme = const ColorScheme.light().copyWith(onSurfaceVariant: const Color(0xffe91e60));
// Brightness.dark
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(colorScheme: lightScheme, useMaterial3: true,),
home: Scaffold(
body: IconTheme.merge(
data: const IconThemeData(size: 26),
child: IconButton(
icon: const Icon(Icons.account_box),
onPressed: () {},
),
),
)
)
);
Color? iconColor0() => _iconStyle(tester, Icons.account_box)?.color;
expect(iconColor0(), lightScheme.onSurfaceVariant); // onSurfaceVariant
});
testWidgets('black87 icon color defined by users shows correctly in Material3', (WidgetTester tester) async {
});
});
}
Widget wrap({required Widget child, required bool useMaterial3}) {
......
// 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Passing no IconButtonTheme returns defaults', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme, useMaterial3: true),
home: Scaffold(
body: Center(
child: IconButton(
onPressed: () { },
icon: const Icon(Icons.ac_unit),
),
),
),
),
);
final Finder buttonMaterial = find.descendant(
of: find.byType(IconButton),
matching: find.byType(Material),
);
final Material material = tester.widget<Material>(buttonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shadowColor, null);
expect(material.shape, const StadiumBorder());
expect(material.textStyle, null);
expect(material.type, MaterialType.button);
final Align align = tester.firstWidget<Align>(find.ancestor(of: find.byIcon(Icons.ac_unit), matching: find.byType(Align)));
expect(align.alignment, Alignment.center);
});
group('[Theme, IconTheme, IconButton style overrides]', () {
const Color foregroundColor = Color(0xff000001);
const Color disabledForegroundColor = Color(0xff000002);
const Color backgroundColor = Color(0xff000003);
const Color shadowColor = Color(0xff000004);
const double elevation = 3;
const EdgeInsets padding = EdgeInsets.all(3);
const Size minimumSize = Size(200, 200);
const BorderSide side = BorderSide(color: Colors.green, width: 2);
const OutlinedBorder shape = RoundedRectangleBorder(side: side, borderRadius: BorderRadius.all(Radius.circular(2)));
const MouseCursor enabledMouseCursor = SystemMouseCursors.text;
const MouseCursor disabledMouseCursor = SystemMouseCursors.grab;
const MaterialTapTargetSize tapTargetSize = MaterialTapTargetSize.shrinkWrap;
const Duration animationDuration = Duration(milliseconds: 25);
const bool enableFeedback = false;
const AlignmentGeometry alignment = Alignment.centerLeft;
final ButtonStyle style = IconButton.styleFrom(
foregroundColor: foregroundColor,
disabledForegroundColor: disabledForegroundColor,
backgroundColor: backgroundColor,
shadowColor: shadowColor,
elevation: elevation,
padding: padding,
minimumSize: minimumSize,
side: side,
shape: shape,
enabledMouseCursor: enabledMouseCursor,
disabledMouseCursor: disabledMouseCursor,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
alignment: alignment,
);
Widget buildFrame({ ButtonStyle? buttonStyle, ButtonStyle? themeStyle, ButtonStyle? overallStyle }) {
final Widget child = Builder(
builder: (BuildContext context) {
return IconButton(
style: buttonStyle,
onPressed: () { },
icon: const Icon(Icons.ac_unit),
);
},
);
return MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true).copyWith(
iconButtonTheme: IconButtonThemeData(style: overallStyle),
),
home: Scaffold(
body: Center(
// If the IconButtonTheme widget is present, it's used
// instead of the Theme's ThemeData.iconButtonTheme.
child: themeStyle == null ? child : IconButtonTheme(
data: IconButtonThemeData(style: themeStyle),
child: child,
),
),
),
);
}
final Finder findMaterial = find.descendant(
of: find.byType(IconButton),
matching: find.byType(Material),
);
final Finder findInkWell = find.descendant(
of: find.byType(IconButton),
matching: find.byType(InkWell),
);
const Set<MaterialState> enabled = <MaterialState>{};
const Set<MaterialState> disabled = <MaterialState>{ MaterialState.disabled };
const Set<MaterialState> hovered = <MaterialState>{ MaterialState.hovered };
const Set<MaterialState> focused = <MaterialState>{ MaterialState.focused };
const Set<MaterialState> pressed = <MaterialState>{ MaterialState.pressed };
void checkButton(WidgetTester tester) {
final Material material = tester.widget<Material>(findMaterial);
final InkWell inkWell = tester.widget<InkWell>(findInkWell);
expect(material.textStyle, null);
expect(material.color, backgroundColor);
expect(material.shadowColor, shadowColor);
expect(material.elevation, elevation);
expect(MaterialStateProperty.resolveAs<MouseCursor?>(inkWell.mouseCursor, enabled), enabledMouseCursor);
expect(MaterialStateProperty.resolveAs<MouseCursor?>(inkWell.mouseCursor, disabled), disabledMouseCursor);
expect(inkWell.overlayColor!.resolve(hovered), foregroundColor.withOpacity(0.08));
expect(inkWell.overlayColor!.resolve(focused), foregroundColor.withOpacity(0.08));
expect(inkWell.overlayColor!.resolve(pressed), foregroundColor.withOpacity(0.12));
expect(inkWell.enableFeedback, enableFeedback);
expect(material.borderRadius, null);
expect(material.shape, shape);
expect(material.animationDuration, animationDuration);
expect(tester.getSize(find.byType(IconButton)), const Size(200, 200));
final Align align = tester.firstWidget<Align>(find.ancestor(of: find.byIcon(Icons.ac_unit), matching: find.byType(Align)));
expect(align.alignment, alignment);
}
testWidgets('Button style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: style));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Button theme style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(themeStyle: style));
await tester.pumpAndSettle();
checkButton(tester);
});
testWidgets('Overall Theme button theme style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(overallStyle: style));
await tester.pumpAndSettle();
checkButton(tester);
});
// Same as the previous tests with empty ButtonStyle's instead of null.
testWidgets('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: style, themeStyle: const ButtonStyle(), overallStyle: const ButtonStyle()));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), themeStyle: style, overallStyle: const ButtonStyle()));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), overallStyle: style));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
});
testWidgets('Theme shadowColor', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
const Color shadowColor = Color(0xff000001);
const Color overriddenColor = Color(0xff000002);
Widget buildFrame({ Color? overallShadowColor, Color? themeShadowColor, Color? shadowColor }) {
return MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme, useMaterial3: true).copyWith(
shadowColor: overallShadowColor,
),
home: Scaffold(
body: Center(
child: IconButtonTheme(
data: IconButtonThemeData(
style: IconButton.styleFrom(
shadowColor: themeShadowColor,
),
),
child: Builder(
builder: (BuildContext context) {
return IconButton(
style: IconButton.styleFrom(
shadowColor: shadowColor,
),
onPressed: () { },
icon: const Icon(Icons.add),
);
},
),
),
),
),
);
}
final Finder buttonMaterialFinder = find.descendant(
of: find.byType(IconButton),
matching: find.byType(Material),
);
await tester.pumpWidget(buildFrame());
Material material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, null); //default
await tester.pumpWidget(buildFrame(overallShadowColor: shadowColor));
await tester.pumpAndSettle(); // theme animation
material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, null);
await tester.pumpWidget(buildFrame(themeShadowColor: shadowColor));
await tester.pumpAndSettle(); // theme animation
material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, shadowColor);
await tester.pumpWidget(buildFrame(shadowColor: shadowColor));
await tester.pumpAndSettle(); // theme animation
material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, shadowColor);
await tester.pumpWidget(buildFrame(overallShadowColor: overriddenColor, themeShadowColor: shadowColor));
await tester.pumpAndSettle(); // theme animation
material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, shadowColor);
await tester.pumpWidget(buildFrame(themeShadowColor: overriddenColor, shadowColor: shadowColor));
await tester.pumpAndSettle(); // theme animation
material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, shadowColor);
});
}
......@@ -2511,10 +2511,10 @@ void main() {
}
await buildFrame();
expect(tester.widget<IconButton>(find.byType(IconButton)).iconSize, 24);
expect(tester.getSize(find.byIcon(Icons.adaptive.more)), const Size(24, 24));
await buildFrame(iconSize: 50);
expect(tester.widget<IconButton>(find.byType(IconButton)).iconSize, 50);
expect(tester.getSize(find.byIcon(Icons.adaptive.more)), const Size(50, 50));
});
testWidgets('does not crash in small overlay', (WidgetTester tester) async {
......@@ -2867,23 +2867,20 @@ void main() {
// Popup menu with default icon size.
await tester.pumpWidget(buildPopupMenu());
IconButton iconButton = tester.widget(find.widgetWithIcon(IconButton, Icons.more_vert));
// Default PopupMenuButton icon size is 24.0.
expect(iconButton.iconSize, 24.0);
expect(tester.getSize(find.byIcon(Icons.more_vert)), const Size(24.0, 24.0));
// Popup menu with custom theme icon size.
await tester.pumpWidget(buildPopupMenu(themeIconSize: 30.0));
await tester.pumpAndSettle();
iconButton = tester.widget(find.widgetWithIcon(IconButton, Icons.more_vert));
// PopupMenuButton icon inherits IconTheme's size.
expect(iconButton.iconSize, 30.0);
expect(tester.getSize(find.byIcon(Icons.more_vert)), const Size(30.0, 30.0));
// Popup menu with custom icon size.
await tester.pumpWidget(buildPopupMenu(themeIconSize: 30.0, iconSize: 50.0));
await tester.pumpAndSettle();
iconButton = tester.widget(find.widgetWithIcon(IconButton, Icons.more_vert));
// PopupMenuButton icon size overrides IconTheme's size.
expect(iconButton.iconSize, 50.0);
expect(tester.getSize(find.byIcon(Icons.more_vert)), const Size(50.0, 50.0));
});
testWidgets('Popup menu clip behavior', (WidgetTester tester) async {
......
......@@ -697,6 +697,7 @@ void main() {
elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom(backgroundColor: Colors.green)),
expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black),
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.black),
iconButtonTheme: IconButtonThemeData(style: IconButton.styleFrom(foregroundColor: Colors.pink)),
listTileTheme: const ListTileThemeData(),
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.black),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.black),
......@@ -809,6 +810,7 @@ void main() {
elevatedButtonTheme: const ElevatedButtonThemeData(),
expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black),
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.white),
iconButtonTheme: const IconButtonThemeData(),
listTileTheme: const ListTileThemeData(),
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.white),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.white),
......@@ -907,6 +909,7 @@ void main() {
elevatedButtonTheme: otherTheme.elevatedButtonTheme,
expansionTileTheme: otherTheme.expansionTileTheme,
floatingActionButtonTheme: otherTheme.floatingActionButtonTheme,
iconButtonTheme: otherTheme.iconButtonTheme,
listTileTheme: otherTheme.listTileTheme,
navigationBarTheme: otherTheme.navigationBarTheme,
navigationRailTheme: otherTheme.navigationRailTheme,
......@@ -1004,6 +1007,7 @@ void main() {
expect(themeDataCopy.elevatedButtonTheme, equals(otherTheme.elevatedButtonTheme));
expect(themeDataCopy.expansionTileTheme, equals(otherTheme.expansionTileTheme));
expect(themeDataCopy.floatingActionButtonTheme, equals(otherTheme.floatingActionButtonTheme));
expect(themeDataCopy.iconButtonTheme, equals(otherTheme.iconButtonTheme));
expect(themeDataCopy.listTileTheme, equals(otherTheme.listTileTheme));
expect(themeDataCopy.navigationBarTheme, equals(otherTheme.navigationBarTheme));
expect(themeDataCopy.navigationRailTheme, equals(otherTheme.navigationRailTheme));
......@@ -1138,6 +1142,7 @@ void main() {
'drawerTheme',
'elevatedButtonTheme',
'floatingActionButtonTheme',
'iconButtonTheme',
'listTileTheme',
'navigationBarTheme',
'navigationRailTheme',
......
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