From d339e50229531b5028e94d22149198857e3054cc Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Tue, 2 Aug 2022 12:22:12 -0700 Subject: [PATCH 1/6] Fixed comments so the IconTheme will still work in M3 --- .../lib/icon_button_template.dart | 4 + packages/flutter/lib/material.dart | 1 + .../flutter/lib/src/material/constants.dart | 10 + .../flutter/lib/src/material/icon_button.dart | 140 ++++++---- .../lib/src/material/icon_button_theme.dart | 123 +++++++++ .../flutter/lib/src/material/popup_menu.dart | 2 +- .../flutter/lib/src/material/theme_data.dart | 20 +- .../flutter/test/material/app_bar_test.dart | 221 ++++++++++++++- .../test/material/icon_button_test.dart | 205 ++++++++++++++ .../test/material/icon_button_theme_test.dart | 251 ++++++++++++++++++ .../test/material/theme_data_test.dart | 5 + 11 files changed, 931 insertions(+), 51 deletions(-) create mode 100644 packages/flutter/lib/src/material/icon_button_theme.dart create mode 100644 packages/flutter/test/material/icon_button_theme_test.dart diff --git a/dev/tools/gen_defaults/lib/icon_button_template.dart b/dev/tools/gen_defaults/lib/icon_button_template.dart index 325c4c59e4c10..18e911c835f97 100644 --- a/dev/tools/gen_defaults/lib/icon_button_template.dart +++ b/dev/tools/gen_defaults/lib/icon_button_template.dart @@ -87,6 +87,10 @@ class _${blockName}DefaultsM3 extends ButtonStyle { @override MaterialStateProperty? get maximumSize => ButtonStyleButton.allOrNull(Size.infinite); + + @override + MaterialStateProperty? get iconSize => + ButtonStyleButton.allOrNull(${tokens["md.comp.icon-button.icon.size"]}); // No default side diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index 164dbe31cdd7f..88edac4bb2d70 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -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'; diff --git a/packages/flutter/lib/src/material/constants.dart b/packages/flutter/lib/src/material/constants.dart index c673daf519dda..bf4ee607dd99c 100644 --- a/packages/flutter/lib/src/material/constants.dart +++ b/packages/flutter/lib/src/material/constants.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,11 @@ 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 [IconTheme] with light brightness. This color is used in +/// [IconButton] to detect whether it is the default value of [ThemeData.iconTheme]. +const Color kDefaultIconLightColor = Colors.white; + +/// The default color for [IconTheme] with dark brightness. This color is used in +/// [IconButton] to detect whether it is the default value of [ThemeData.iconTheme]. +const Color kDefaultIconDarkColor = Colors.black87; diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart index df456ed848aa7..898b85f0947d6 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -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,8 @@ 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 it becomes 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 +111,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] is used to override the default style +/// of [IconButton]. If both themes exist, the [IconButtonTheme] will override [IconTheme] no matter +/// which is closer to [IconButton]. Each [IconButton]'s property is resolved by the order of +/// precedence: widget property, [IconButtonTheme] property, [IconTheme] property and +/// Material 3 [ButtonStyle] default property. +/// /// {@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 +147,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 exist 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 +158,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 +172,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 +193,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 default size + /// comes from Material 3 default [ButtonStyle]. 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 +212,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 +225,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 +363,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 +475,7 @@ class IconButton extends StatelessWidget { Size? minimumSize, Size? fixedSize, Size? maximumSize, + double? iconSize, BorderSide? side, OutlinedBorder? shape, EdgeInsetsGeometry? padding, @@ -501,6 +512,7 @@ class IconButton extends StatelessWidget { minimumSize: ButtonStyleButton.allOrNull(minimumSize), fixedSize: ButtonStyleButton.allOrNull(fixedSize), maximumSize: ButtonStyleButton.allOrNull(maximumSize), + iconSize: ButtonStyleButton.allOrNull(iconSize), side: ButtonStyleButton.allOrNull(side), shape: ButtonStyleButton.allOrNull(shape), mouseCursor: mouseCursor, @@ -516,25 +528,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 +547,7 @@ class IconButton extends StatelessWidget { padding: padding, minimumSize: minSize, maximumSize: maxSize, + iconSize: iconSize, alignment: alignment, enabledMouseCursor: mouseCursor, disabledMouseCursor: mouseCursor, @@ -568,16 +562,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 +580,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 +638,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 +772,7 @@ class _IconButtonM3 extends ButtonStyleButton { /// * `minimumSize` - Size(40, 40) /// * `fixedSize` - null /// * `maximumSize` - Size.infinite + /// * `iconSize` - 24 /// * `side` - null /// * `shape` - StadiumBorder() /// * `mouseCursor` @@ -778,10 +789,41 @@ 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 bool isDark = Theme.of(context).brightness == Brightness.dark; + final IconThemeData overallIconTheme = IconTheme.of(context); + + final bool isDefaultIconThemeSize = overallIconTheme.size == const IconThemeData.fallback().size; + final double? iconThemeSize = isDefaultIconThemeSize ? null : overallIconTheme.size; + + bool identical(Color color) { + if (isDark) { + return color == kDefaultIconLightColor; + } + return color == kDefaultIconDarkColor; + } + final bool isDefaultIconThemeColor = identical(overallIconTheme.color!); + final Color? iconThemeColor = isDefaultIconThemeColor ? null : overallIconTheme.color; + + ButtonStyle? iconButtonThemeStyle = IconButtonTheme.of(context).style; + if (iconButtonThemeStyle == null) { + return IconButton.styleFrom(foregroundColor: iconThemeColor, iconSize: iconThemeSize); + } + if (iconButtonThemeStyle.iconSize == null) { + iconButtonThemeStyle = iconButtonThemeStyle.copyWith(iconSize: ButtonStyleButton.allOrNull(iconThemeSize)); + } + if (iconButtonThemeStyle.foregroundColor == null) { + final MaterialStateProperty? foregroundColor = iconThemeColor == null + ? null + : _IconButtonDefaultForeground(iconThemeColor, null); + iconButtonThemeStyle = iconButtonThemeStyle.copyWith(foregroundColor: foregroundColor); + } + return iconButtonThemeStyle; } } @@ -968,6 +1010,10 @@ class _IconButtonDefaultsM3 extends ButtonStyle { @override MaterialStateProperty? get maximumSize => ButtonStyleButton.allOrNull(Size.infinite); + + @override + MaterialStateProperty? get iconSize => + ButtonStyleButton.allOrNull(24.0); // No default side diff --git a/packages/flutter/lib/src/material/icon_button_theme.dart b/packages/flutter/lib/src/material/icon_button_theme.dart new file mode 100644 index 0000000000000..cbb72c155b304 --- /dev/null +++ b/packages/flutter/lib/src/material/icon_button_theme.dart @@ -0,0 +1,123 @@ +// 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 [IconButtonTheme] or with 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('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(); + 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; +} diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 372ddb277693b..22878ac579b7c 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -1265,7 +1265,7 @@ class PopupMenuButtonState extends State> { 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, diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 29ae6d5e5dcef..9ba7539577f86 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -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('elevatedButtonTheme', elevatedButtonTheme, defaultValue: defaultData.elevatedButtonTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('expansionTileTheme', expansionTileTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('floatingActionButtonTheme', floatingActionButtonTheme, defaultValue: defaultData.floatingActionButtonTheme, level: DiagnosticLevel.debug)); + properties.add(DiagnosticsProperty('iconButtonTheme', iconButtonTheme, defaultValue: defaultData.iconButtonTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('listTileTheme', listTileTheme, defaultValue: defaultData.listTileTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('navigationBarTheme', navigationBarTheme, defaultValue: defaultData.navigationBarTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('navigationRailTheme', navigationRailTheme, defaultValue: defaultData.navigationRailTheme, level: DiagnosticLevel.debug)); diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index b1ea29cc771c4..620d6478600db 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.dart @@ -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( + 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: [Icon(Icons.add_circle, key: actionIconKey), const Text('action')], + actions: [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: [ + 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: [ + 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: [ + 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: [Icon(Icons.add_circle, key: actionIconKey), const Text('action')], + actions: [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', () { diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart index b8c07e62e01dc..7041f324c3a8e 100644 --- a/packages/flutter/test/material/icon_button_test.dart +++ b/packages/flutter/test/material/icon_button_test.dart @@ -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(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( @@ -1013,6 +1029,7 @@ void main() { final Align align = tester.firstWidget(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 +1586,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}) { diff --git a/packages/flutter/test/material/icon_button_theme_test.dart b/packages/flutter/test/material/icon_button_theme_test.dart new file mode 100644 index 0000000000000..44fd8a6d7e228 --- /dev/null +++ b/packages/flutter/test/material/icon_button_theme_test.dart @@ -0,0 +1,251 @@ +// 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(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(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 enabled = {}; + const Set disabled = { MaterialState.disabled }; + const Set hovered = { MaterialState.hovered }; + const Set focused = { MaterialState.focused }; + const Set pressed = { MaterialState.pressed }; + + void checkButton(WidgetTester tester) { + final Material material = tester.widget(findMaterial); + final InkWell inkWell = tester.widget(findInkWell); + expect(material.textStyle, null); + expect(material.color, backgroundColor); + expect(material.shadowColor, shadowColor); + expect(material.elevation, elevation); + expect(MaterialStateProperty.resolveAs(inkWell.mouseCursor, enabled), enabledMouseCursor); + expect(MaterialStateProperty.resolveAs(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(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(buttonMaterialFinder); + expect(material.shadowColor, null); //default + + await tester.pumpWidget(buildFrame(overallShadowColor: shadowColor)); + await tester.pumpAndSettle(); // theme animation + material = tester.widget(buttonMaterialFinder); + expect(material.shadowColor, null); + + await tester.pumpWidget(buildFrame(themeShadowColor: shadowColor)); + await tester.pumpAndSettle(); // theme animation + material = tester.widget(buttonMaterialFinder); + expect(material.shadowColor, shadowColor); + + await tester.pumpWidget(buildFrame(shadowColor: shadowColor)); + await tester.pumpAndSettle(); // theme animation + material = tester.widget(buttonMaterialFinder); + expect(material.shadowColor, shadowColor); + + await tester.pumpWidget(buildFrame(overallShadowColor: overriddenColor, themeShadowColor: shadowColor)); + await tester.pumpAndSettle(); // theme animation + material = tester.widget(buttonMaterialFinder); + expect(material.shadowColor, shadowColor); + + await tester.pumpWidget(buildFrame(themeShadowColor: overriddenColor, shadowColor: shadowColor)); + await tester.pumpAndSettle(); // theme animation + material = tester.widget(buttonMaterialFinder); + expect(material.shadowColor, shadowColor); + }); +} diff --git a/packages/flutter/test/material/theme_data_test.dart b/packages/flutter/test/material/theme_data_test.dart index 1162969577e00..0cb5ab4a96da8 100644 --- a/packages/flutter/test/material/theme_data_test.dart +++ b/packages/flutter/test/material/theme_data_test.dart @@ -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', From 1a38a3ab7f91f47fd70a7ad5d7bb28fe63128a3c Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Tue, 2 Aug 2022 12:55:51 -0700 Subject: [PATCH 2/6] Fixed failing tests and pr comment --- .../flutter/lib/src/material/icon_button_theme.dart | 2 +- packages/flutter/test/material/popup_menu_test.dart | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/material/icon_button_theme.dart b/packages/flutter/lib/src/material/icon_button_theme.dart index cbb72c155b304..5480b37c64a7c 100644 --- a/packages/flutter/lib/src/material/icon_button_theme.dart +++ b/packages/flutter/lib/src/material/icon_button_theme.dart @@ -9,7 +9,7 @@ import 'button_style.dart'; import 'theme.dart'; /// A [ButtonStyle] that overrides the default appearance of -/// [IconButton]s when it's used with [IconButtonTheme] or with the +/// [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] diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart index cfac72adc31bc..cbe78564cc615 100644 --- a/packages/flutter/test/material/popup_menu_test.dart +++ b/packages/flutter/test/material/popup_menu_test.dart @@ -2511,10 +2511,10 @@ void main() { } await buildFrame(); - expect(tester.widget(find.byType(IconButton)).iconSize, 24); + expect(tester.getSize(find.byIcon(Icons.adaptive.more)), const Size(24, 24)); await buildFrame(iconSize: 50); - expect(tester.widget(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 { @@ -2869,21 +2869,21 @@ void main() { 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 { From ecc1cc370ebb8496d1c46098b553bca5d0d2c3f5 Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Tue, 2 Aug 2022 13:14:23 -0700 Subject: [PATCH 3/6] fixed grammar --- packages/flutter/lib/src/material/icon_button.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart index 898b85f0947d6..115bea2208aca 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -111,9 +111,9 @@ 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] is used to override the default style +/// 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 [IconButton]. Each [IconButton]'s property is resolved by the order of +/// which is closer to the [IconButton]. Each [IconButton]'s property is resolved by the order of /// precedence: widget property, [IconButtonTheme] property, [IconTheme] property and /// Material 3 [ButtonStyle] default property. /// From e39995b2b9399dff1fcbdf42ca6cbfaa561fc4fc Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Tue, 2 Aug 2022 13:21:55 -0700 Subject: [PATCH 4/6] Removed trailing spaces --- dev/tools/gen_defaults/lib/icon_button_template.dart | 2 +- packages/flutter/lib/src/material/icon_button.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/tools/gen_defaults/lib/icon_button_template.dart b/dev/tools/gen_defaults/lib/icon_button_template.dart index 18e911c835f97..0e8a9b5dc8d52 100644 --- a/dev/tools/gen_defaults/lib/icon_button_template.dart +++ b/dev/tools/gen_defaults/lib/icon_button_template.dart @@ -87,7 +87,7 @@ class _${blockName}DefaultsM3 extends ButtonStyle { @override MaterialStateProperty? get maximumSize => ButtonStyleButton.allOrNull(Size.infinite); - + @override MaterialStateProperty? get iconSize => ButtonStyleButton.allOrNull(${tokens["md.comp.icon-button.icon.size"]}); diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart index 115bea2208aca..4294a76991653 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -1010,7 +1010,7 @@ class _IconButtonDefaultsM3 extends ButtonStyle { @override MaterialStateProperty? get maximumSize => ButtonStyleButton.allOrNull(Size.infinite); - + @override MaterialStateProperty? get iconSize => ButtonStyleButton.allOrNull(24.0); From 4cb767fc4366b675c28c59fc1f4c8629c8fa462b Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Tue, 2 Aug 2022 13:37:58 -0700 Subject: [PATCH 5/6] Removed unused variables --- packages/flutter/lib/src/material/popup_menu.dart | 2 -- packages/flutter/test/material/popup_menu_test.dart | 3 --- 2 files changed, 5 deletions(-) diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 22878ac579b7c..2b4adedb0c6ca 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -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 extends State> { @override Widget build(BuildContext context) { - final IconThemeData iconTheme = IconTheme.of(context); final bool enableFeedback = widget.enableFeedback ?? PopupMenuTheme.of(context).enableFeedback ?? true; diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart index cbe78564cc615..80e1b86d48d43 100644 --- a/packages/flutter/test/material/popup_menu_test.dart +++ b/packages/flutter/test/material/popup_menu_test.dart @@ -2867,21 +2867,18 @@ 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(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(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(tester.getSize(find.byIcon(Icons.more_vert)), const Size(50.0, 50.0)); }); From f76820c2dd657b5bd07cb20a404d650e264b5702 Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Wed, 3 Aug 2022 10:50:35 -0700 Subject: [PATCH 6/6] Fixed comments --- .../flutter/lib/src/material/constants.dart | 10 +++-- .../flutter/lib/src/material/icon_button.dart | 44 +++++++------------ .../test/material/icon_button_test.dart | 42 ++++++++++++++++++ 3 files changed, 65 insertions(+), 31 deletions(-) diff --git a/packages/flutter/lib/src/material/constants.dart b/packages/flutter/lib/src/material/constants.dart index bf4ee607dd99c..4696cce72d6a5 100644 --- a/packages/flutter/lib/src/material/constants.dart +++ b/packages/flutter/lib/src/material/constants.dart @@ -50,10 +50,12 @@ 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 [IconTheme] with light brightness. This color is used in -/// [IconButton] to detect whether it is the default value of [ThemeData.iconTheme]. +/// 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 [IconTheme] with dark brightness. This color is used in -/// [IconButton] to detect whether it is the default value of [ThemeData.iconTheme]. +/// 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; diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart index 4294a76991653..a72c91083e544 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -39,7 +39,8 @@ const double _kMinButtonSize = kMinInteractiveDimension; /// will not react to touch. /// /// Requires one of its ancestors to be a [Material] widget. In Material Design 3, -/// this requirement no longer exists because it becomes a subclass of [ButtonStyleButton]. +/// 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 @@ -115,7 +116,7 @@ const double _kMinButtonSize = kMinInteractiveDimension; /// 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 -/// Material 3 [ButtonStyle] default property. +/// internal default property value. /// /// {@tool dartpad} /// This sample shows creation of [IconButton] widgets for standard, filled, @@ -148,7 +149,7 @@ class IconButton extends StatelessWidget { /// be used in many other places as well. /// /// Requires one of its ancestors to be a [Material] widget. This requirement - /// no longer exist if [ThemeData.useMaterial3] is set to true. + /// no longer exists if [ThemeData.useMaterial3] is set to true. /// /// [autofocus] argument must not be null (though it has default value). /// @@ -194,9 +195,9 @@ class IconButton extends StatelessWidget { /// [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 default size - /// comes from Material 3 default [ButtonStyle]. The size given here is passed - /// down to the [ButtonStyle.iconSize] property. + /// 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. @@ -795,35 +796,24 @@ class _IconButtonM3 extends ButtonStyleButton { /// in both [IconButtonTheme] and [IconTheme], [IconTheme] will be overridden. @override ButtonStyle? themeStyleOf(BuildContext context) { + final IconThemeData iconTheme = IconTheme.of(context); final bool isDark = Theme.of(context).brightness == Brightness.dark; - final IconThemeData overallIconTheme = IconTheme.of(context); - final bool isDefaultIconThemeSize = overallIconTheme.size == const IconThemeData.fallback().size; - final double? iconThemeSize = isDefaultIconThemeSize ? null : overallIconTheme.size; - - bool identical(Color color) { + bool isIconThemeDefault(Color? color) { if (isDark) { return color == kDefaultIconLightColor; } return color == kDefaultIconDarkColor; } - final bool isDefaultIconThemeColor = identical(overallIconTheme.color!); - final Color? iconThemeColor = isDefaultIconThemeColor ? null : overallIconTheme.color; + final bool isDefaultColor = isIconThemeDefault(iconTheme.color); + final bool isDefaultSize = iconTheme.size == const IconThemeData.fallback().size; - ButtonStyle? iconButtonThemeStyle = IconButtonTheme.of(context).style; - if (iconButtonThemeStyle == null) { - return IconButton.styleFrom(foregroundColor: iconThemeColor, iconSize: iconThemeSize); - } - if (iconButtonThemeStyle.iconSize == null) { - iconButtonThemeStyle = iconButtonThemeStyle.copyWith(iconSize: ButtonStyleButton.allOrNull(iconThemeSize)); - } - if (iconButtonThemeStyle.foregroundColor == null) { - final MaterialStateProperty? foregroundColor = iconThemeColor == null - ? null - : _IconButtonDefaultForeground(iconThemeColor, null); - iconButtonThemeStyle = iconButtonThemeStyle.copyWith(foregroundColor: foregroundColor); - } - return iconButtonThemeStyle; + final ButtonStyle iconThemeStyle = IconButton.styleFrom( + foregroundColor: isDefaultColor ? null : iconTheme.color, + iconSize: isDefaultSize ? null : iconTheme.size + ); + + return IconButtonTheme.of(context).style?.merge(iconThemeStyle) ?? iconThemeStyle; } } diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart index 7041f324c3a8e..271db0f22ea13 100644 --- a/packages/flutter/test/material/icon_button_test.dart +++ b/packages/flutter/test/material/icon_button_test.dart @@ -993,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);