From 1df35e6b236ae7ed0bd1fed05afb96c4dfa9deea Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Wed, 17 Jun 2026 13:45:24 -0700 Subject: [PATCH 1/7] Add global Material style variant --- packages/material_ui/lib/src/theme_data.dart | 44 ++++++++++++++++--- .../material_ui/test/theme_data_test.dart | 44 ++++++++++++++++++- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/packages/material_ui/lib/src/theme_data.dart b/packages/material_ui/lib/src/theme_data.dart index b4c7e1e0b145..dcd8fccabd5e 100644 --- a/packages/material_ui/lib/src/theme_data.dart +++ b/packages/material_ui/lib/src/theme_data.dart @@ -182,6 +182,15 @@ enum MaterialTapTargetSize { shrinkWrap, } +/// Defines the Material Design style variant used by Material components. +enum StyleVariant { + /// The Material Design 3 style variant. + material3, + + /// The Material Design 3 Expressive style variant. + material3Expressive, +} + /// Defines the configuration of the overall visual [Theme] for a [MaterialApp] /// or a widget subtree within the app. /// @@ -282,6 +291,7 @@ class ThemeData with Diagnosticable { InteractiveInkFeatureFactory? splashFactory, bool? useMaterial3, bool? useSystemColors, + StyleVariant? variant, VisualDensity? visualDensity, // COLOR ColorScheme? colorScheme, @@ -385,6 +395,7 @@ class ThemeData with Diagnosticable { cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault(); extensions ??= >[]; adaptations ??= >[]; + variant ??= StyleVariant.material3; // TODO(bleroux): Clean this up once the type of `inputDecorationTheme` is changed to `InputDecorationThemeData` if (inputDecorationTheme != null) { if (inputDecorationTheme is InputDecorationTheme) { @@ -605,6 +616,7 @@ class ThemeData with Diagnosticable { scrollbarTheme: scrollbarTheme, splashFactory: splashFactory, useMaterial3: useMaterial3, + variant: variant, visualDensity: visualDensity, // COLOR canvasColor: canvasColor, @@ -715,6 +727,7 @@ class ThemeData with Diagnosticable { required this.scrollbarTheme, required this.splashFactory, required this.useMaterial3, + required this.variant, required this.visualDensity, // COLOR required this.colorScheme, @@ -841,6 +854,7 @@ class ThemeData with Diagnosticable { required ColorScheme colorScheme, TextTheme? textTheme, bool? useMaterial3, + StyleVariant? variant, }) { final isDark = colorScheme.brightness == Brightness.dark; @@ -861,6 +875,7 @@ class ThemeData with Diagnosticable { textTheme: textTheme, applyElevationOverlayColor: isDark, useMaterial3: useMaterial3, + variant: variant, ); } @@ -868,15 +883,15 @@ class ThemeData with Diagnosticable { /// /// This theme does not contain text geometry. Instead, it is expected that /// this theme is localized using text geometry using [ThemeData.localize]. - factory ThemeData.light({bool? useMaterial3}) => - ThemeData(brightness: Brightness.light, useMaterial3: useMaterial3); + factory ThemeData.light({bool? useMaterial3, StyleVariant? variant}) => + ThemeData(brightness: Brightness.light, useMaterial3: useMaterial3, variant: variant); /// A default dark theme. /// /// This theme does not contain text geometry. Instead, it is expected that /// this theme is localized using text geometry using [ThemeData.localize]. - factory ThemeData.dark({bool? useMaterial3}) => - ThemeData(brightness: Brightness.dark, useMaterial3: useMaterial3); + factory ThemeData.dark({bool? useMaterial3, StyleVariant? variant}) => + ThemeData(brightness: Brightness.dark, useMaterial3: useMaterial3, variant: variant); /// The default color theme. Same as [ThemeData.light]. /// @@ -887,7 +902,8 @@ class ThemeData with Diagnosticable { /// /// Most applications would use [Theme.of], which provides correct localized /// text geometry. - factory ThemeData.fallback({bool? useMaterial3}) => ThemeData.light(useMaterial3: useMaterial3); + factory ThemeData.fallback({bool? useMaterial3, StyleVariant? variant}) => + ThemeData.light(useMaterial3: useMaterial3, variant: variant); /// Used to obtain a particular [Adaptation] from [adaptationMap]. /// @@ -1145,6 +1161,11 @@ class ThemeData with Diagnosticable { /// * [Material 3 specification](https://m3.material.io/). final bool useMaterial3; + /// The Material style variant used by Material components. + /// + /// Defaults to [StyleVariant.material3]. + final StyleVariant variant; + /// The density value for specifying the compactness of various UI components. /// /// {@template flutter.material.themedata.visualDensity} @@ -1498,6 +1519,7 @@ class ThemeData with Diagnosticable { TargetPlatform? platform, ScrollbarThemeData? scrollbarTheme, InteractiveInkFeatureFactory? splashFactory, + StyleVariant? variant, VisualDensity? visualDensity, // COLOR ColorScheme? colorScheme, @@ -1634,6 +1656,7 @@ class ThemeData with Diagnosticable { // When deprecated useMaterial3 removed, maintain `this.useMaterial3` here // for == evaluation. useMaterial3: useMaterial3 ?? this.useMaterial3, + variant: variant ?? this.variant, visualDensity: visualDensity ?? this.visualDensity, // COLOR canvasColor: canvasColor ?? this.canvasColor, @@ -1959,6 +1982,7 @@ class ThemeData with Diagnosticable { scrollbarTheme: ScrollbarThemeData.lerp(a.scrollbarTheme, b.scrollbarTheme, t), splashFactory: t < 0.5 ? a.splashFactory : b.splashFactory, useMaterial3: t < 0.5 ? a.useMaterial3 : b.useMaterial3, + variant: t < 0.5 ? a.variant : b.variant, visualDensity: VisualDensity.lerp(a.visualDensity, b.visualDensity, t), // COLOR canvasColor: Color.lerp(a.canvasColor, b.canvasColor, t)!, @@ -2107,6 +2131,7 @@ class ThemeData with Diagnosticable { other.scrollbarTheme == scrollbarTheme && other.splashFactory == splashFactory && other.useMaterial3 == useMaterial3 && + other.variant == variant && other.visualDensity == visualDensity && // COLOR other.canvasColor == canvasColor && @@ -2207,6 +2232,7 @@ class ThemeData with Diagnosticable { scrollbarTheme, splashFactory, useMaterial3, + variant, visualDensity, // COLOR canvasColor, @@ -2381,6 +2407,14 @@ class ThemeData with Diagnosticable { level: DiagnosticLevel.debug, ), ); + properties.add( + EnumProperty( + 'variant', + variant, + defaultValue: defaultData.variant, + level: DiagnosticLevel.debug, + ), + ); properties.add( DiagnosticsProperty( 'visualDensity', diff --git a/packages/material_ui/test/theme_data_test.dart b/packages/material_ui/test/theme_data_test.dart index ca97ec327c0f..1b2520656c8c 100644 --- a/packages/material_ui/test/theme_data_test.dart +++ b/packages/material_ui/test/theme_data_test.dart @@ -24,6 +24,43 @@ void main() { expect(dawn.primaryColor, Color.lerp(dark.primaryColor, light.primaryColor, 0.25)); }); + test('ThemeData supports style variants', () { + expect(ThemeData().variant, StyleVariant.material3); + expect( + ThemeData(variant: StyleVariant.material3Expressive).variant, + StyleVariant.material3Expressive, + ); + expect( + ThemeData.light(variant: StyleVariant.material3Expressive).variant, + StyleVariant.material3Expressive, + ); + expect( + ThemeData.dark(variant: StyleVariant.material3Expressive).variant, + StyleVariant.material3Expressive, + ); + expect( + ThemeData.fallback(variant: StyleVariant.material3Expressive).variant, + StyleVariant.material3Expressive, + ); + }); + + test('ThemeData.copyWith supports style variants', () { + final theme = ThemeData(); + final ThemeData expressiveTheme = theme.copyWith(variant: StyleVariant.material3Expressive); + + expect(expressiveTheme.variant, StyleVariant.material3Expressive); + expect(expressiveTheme, isNot(theme)); + expect(expressiveTheme.hashCode, isNot(theme.hashCode)); + }); + + test('ThemeData.lerp switches style variants discretely', () { + final material3 = ThemeData(variant: StyleVariant.material3); + final expressive = ThemeData(variant: StyleVariant.material3Expressive); + + expect(ThemeData.lerp(material3, expressive, 0.25).variant, StyleVariant.material3); + expect(ThemeData.lerp(material3, expressive, 0.75).variant, StyleVariant.material3Expressive); + }); + test('ThemeData objects with .styleFrom() members are equal', () { ThemeData createThemeData() { return ThemeData( @@ -1290,6 +1327,7 @@ void main() { scrollbarTheme: const ScrollbarThemeData(radius: Radius.circular(10.0)), splashFactory: InkRipple.splashFactory, useMaterial3: false, + variant: StyleVariant.material3, visualDensity: VisualDensity.standard, // COLOR canvasColor: Colors.black, @@ -1419,6 +1457,7 @@ void main() { scrollbarTheme: const ScrollbarThemeData(radius: Radius.circular(10.0)), splashFactory: InkRipple.splashFactory, useMaterial3: true, + variant: StyleVariant.material3Expressive, visualDensity: VisualDensity.standard, // COLOR canvasColor: Colors.white, @@ -1523,6 +1562,7 @@ void main() { scrollbarTheme: otherTheme.scrollbarTheme, splashFactory: otherTheme.splashFactory, useMaterial3: otherTheme.useMaterial3, + variant: otherTheme.variant, visualDensity: otherTheme.visualDensity, // COLOR canvasColor: otherTheme.canvasColor, @@ -1615,6 +1655,7 @@ void main() { expect(themeDataCopy.scrollbarTheme, equals(otherTheme.scrollbarTheme)); expect(themeDataCopy.splashFactory, equals(otherTheme.splashFactory)); expect(themeDataCopy.useMaterial3, equals(otherTheme.useMaterial3)); + expect(themeDataCopy.variant, equals(otherTheme.variant)); expect(themeDataCopy.visualDensity, equals(otherTheme.visualDensity)); // COLOR expect(themeDataCopy.canvasColor, equals(otherTheme.canvasColor)); @@ -1761,8 +1802,9 @@ void main() { 'platform', 'scrollbarTheme', 'splashFactory', - 'visualDensity', 'useMaterial3', + 'variant', + 'visualDensity', // COLOR 'colorScheme', 'primaryColor', From 106459acc55c5282120e3680d8011a082055087a Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Thu, 18 Jun 2026 15:38:51 -0700 Subject: [PATCH 2/7] Add assertion to component theme and component build methods --- packages/material_ui/lib/src/app_bar.dart | 2 ++ packages/material_ui/lib/src/app_bar_theme.dart | 15 +++++++++++++-- .../material_ui/lib/src/elevated_button.dart | 2 ++ .../lib/src/elevated_button_theme.dart | 16 ++++++++++++---- packages/material_ui/lib/src/filled_button.dart | 5 ++++- .../material_ui/lib/src/filled_button_theme.dart | 16 ++++++++++++---- .../lib/src/floating_action_button.dart | 2 ++ .../lib/src/floating_action_button_theme.dart | 15 ++++++++++++--- packages/material_ui/lib/src/icon_button.dart | 3 +++ .../material_ui/lib/src/icon_button_theme.dart | 16 ++++++++++++---- packages/material_ui/lib/src/menu_anchor.dart | 15 +++++++++++++++ .../material_ui/lib/src/menu_button_theme.dart | 16 ++++++++++++---- packages/material_ui/lib/src/menu_theme.dart | 15 ++++++++++++--- packages/material_ui/lib/src/navigation_bar.dart | 3 +++ .../lib/src/navigation_bar_theme.dart | 14 ++++++++++++-- .../material_ui/lib/src/navigation_rail.dart | 7 +++++-- .../lib/src/navigation_rail_theme.dart | 14 ++++++++++++-- .../material_ui/lib/src/outlined_button.dart | 2 ++ .../lib/src/outlined_button_theme.dart | 16 ++++++++++++---- .../material_ui/lib/src/progress_indicator.dart | 9 ++++++++- .../lib/src/progress_indicator_theme.dart | 14 ++++++++++++-- packages/material_ui/lib/src/range_slider.dart | 2 ++ packages/material_ui/lib/src/search_anchor.dart | 10 +++++++++- .../material_ui/lib/src/search_bar_theme.dart | 14 ++++++++++++-- .../material_ui/lib/src/search_view_theme.dart | 14 ++++++++++++-- packages/material_ui/lib/src/slider.dart | 2 ++ packages/material_ui/lib/src/slider_theme.dart | 14 ++++++++++++-- packages/material_ui/lib/src/text_button.dart | 2 ++ .../material_ui/lib/src/text_button_theme.dart | 16 ++++++++++++---- packages/material_ui/lib/src/theme.dart | 2 +- packages/material_ui/lib/src/theme_data.dart | 6 ++++-- 31 files changed, 247 insertions(+), 52 deletions(-) diff --git a/packages/material_ui/lib/src/app_bar.dart b/packages/material_ui/lib/src/app_bar.dart index cdbb452b5009..96f6996605fa 100644 --- a/packages/material_ui/lib/src/app_bar.dart +++ b/packages/material_ui/lib/src/app_bar.dart @@ -906,6 +906,8 @@ class _AppBarState extends State { final ThemeData theme = Theme.of(context); final IconButtonThemeData iconButtonTheme = IconButtonTheme.of(context); final AppBarThemeData appBarTheme = AppBarTheme.of(context); + final StyleVariant effectiveVariant = appBarTheme.variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); final AppBarThemeData defaults = theme.useMaterial3 ? _AppBarDefaultsM3(context) : _AppBarDefaultsM2(context); diff --git a/packages/material_ui/lib/src/app_bar_theme.dart b/packages/material_ui/lib/src/app_bar_theme.dart index 93f5ee74dbf4..59d502fd3803 100644 --- a/packages/material_ui/lib/src/app_bar_theme.dart +++ b/packages/material_ui/lib/src/app_bar_theme.dart @@ -410,10 +410,12 @@ class AppBarThemeData with Diagnosticable { this.titleTextStyle, this.systemOverlayStyle, this.actionsPadding, + this.variant, }) : assert( color == null || backgroundColor == null, 'The color and backgroundColor parameters mean the same thing. Only specify one.', - ); + ), + assert(variant != .material3Expressive, 'Only material3 is supported.'); /// Overrides the default value of [AppBar.backgroundColor]. final Color? backgroundColor; @@ -466,6 +468,9 @@ class AppBarThemeData with Diagnosticable { /// Overrides the default value of [AppBar.actionsPadding]. final EdgeInsetsGeometry? actionsPadding; + /// The style variant of Material Design used by [AppBar]. + final StyleVariant? variant; + /// Creates a copy of this object but with the given fields replaced with the /// new values. AppBarThemeData copyWith({ @@ -491,6 +496,7 @@ class AppBarThemeData with Diagnosticable { TextStyle? titleTextStyle, SystemUiOverlayStyle? systemOverlayStyle, EdgeInsetsGeometry? actionsPadding, + StyleVariant? variant, }) { return AppBarThemeData( backgroundColor: backgroundColor ?? color ?? this.backgroundColor, @@ -510,6 +516,7 @@ class AppBarThemeData with Diagnosticable { titleTextStyle: titleTextStyle ?? this.titleTextStyle, systemOverlayStyle: systemOverlayStyle ?? this.systemOverlayStyle, actionsPadding: actionsPadding ?? this.actionsPadding, + variant: variant ?? this.variant, ); } @@ -538,6 +545,7 @@ class AppBarThemeData with Diagnosticable { titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), systemOverlayStyle: t < 0.5 ? a.systemOverlayStyle : b.systemOverlayStyle, actionsPadding: EdgeInsetsGeometry.lerp(a.actionsPadding, b.actionsPadding, t), + variant: t < 0.5 ? a.variant : b.variant, ); } @@ -560,6 +568,7 @@ class AppBarThemeData with Diagnosticable { titleTextStyle, systemOverlayStyle, actionsPadding, + variant, ); @override @@ -587,7 +596,8 @@ class AppBarThemeData with Diagnosticable { other.toolbarTextStyle == toolbarTextStyle && other.titleTextStyle == titleTextStyle && other.systemOverlayStyle == systemOverlayStyle && - other.actionsPadding == actionsPadding; + other.actionsPadding == actionsPadding && + other.variant == variant; } @override @@ -633,5 +643,6 @@ class AppBarThemeData with Diagnosticable { defaultValue: null, ), ); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/elevated_button.dart b/packages/material_ui/lib/src/elevated_button.dart index 25a0af018af3..471082f5a481 100644 --- a/packages/material_ui/lib/src/elevated_button.dart +++ b/packages/material_ui/lib/src/elevated_button.dart @@ -392,6 +392,8 @@ class ElevatedButton extends ButtonStyleButton { @override ButtonStyle defaultStyleOf(BuildContext context) { final ThemeData theme = Theme.of(context); + final StyleVariant effectiveVariant = ElevatedButtonTheme.of(context).variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); final ColorScheme colorScheme = theme.colorScheme; final ButtonStyle buttonStyle = theme.useMaterial3 ? _ElevatedButtonDefaultsM3(context) diff --git a/packages/material_ui/lib/src/elevated_button_theme.dart b/packages/material_ui/lib/src/elevated_button_theme.dart index da92888243ed..490226c2ee06 100644 --- a/packages/material_ui/lib/src/elevated_button_theme.dart +++ b/packages/material_ui/lib/src/elevated_button_theme.dart @@ -39,7 +39,8 @@ class ElevatedButtonThemeData with Diagnosticable { /// Creates an [ElevatedButtonThemeData]. /// /// The [style] may be null. - const ElevatedButtonThemeData({this.style}); + const ElevatedButtonThemeData({this.style, this.variant}) + : assert(variant != .material3Expressive, 'Only material3 is supported.'); /// Overrides for [ElevatedButton]'s default style. /// @@ -50,6 +51,9 @@ class ElevatedButtonThemeData with Diagnosticable { /// If [style] is null, then this theme doesn't override anything. final ButtonStyle? style; + /// The style variant of Material Design used by [ElevatedButton]. + final StyleVariant? variant; + /// Linearly interpolate between two elevated button themes. static ElevatedButtonThemeData? lerp( ElevatedButtonThemeData? a, @@ -59,11 +63,14 @@ class ElevatedButtonThemeData with Diagnosticable { if (identical(a, b)) { return a; } - return ElevatedButtonThemeData(style: ButtonStyle.lerp(a?.style, b?.style, t)); + return ElevatedButtonThemeData( + style: ButtonStyle.lerp(a?.style, b?.style, t), + variant: t < 0.5 ? a?.variant : b?.variant, + ); } @override - int get hashCode => style.hashCode; + int get hashCode => Object.hash(style, variant); @override bool operator ==(Object other) { @@ -73,13 +80,14 @@ class ElevatedButtonThemeData with Diagnosticable { if (other.runtimeType != runtimeType) { return false; } - return other is ElevatedButtonThemeData && other.style == style; + return other is ElevatedButtonThemeData && other.style == style && other.variant == variant; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('style', style, defaultValue: null)); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/filled_button.dart b/packages/material_ui/lib/src/filled_button.dart index 8e252ae39fc8..bcaf9493341b 100644 --- a/packages/material_ui/lib/src/filled_button.dart +++ b/packages/material_ui/lib/src/filled_button.dart @@ -430,13 +430,16 @@ class FilledButton extends ButtonStyleButton { /// [ButtonStyle.padding] is reduced from 24 to 16. @override ButtonStyle defaultStyleOf(BuildContext context) { + final ThemeData theme = Theme.of(context); + final StyleVariant effectiveVariant = FilledButtonTheme.of(context).variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); final ButtonStyle buttonStyle = switch (_variant) { _FilledButtonVariant.filled => _FilledButtonDefaultsM3(context), _FilledButtonVariant.tonal => _FilledTonalButtonDefaultsM3(context), }; if (_addPadding) { - final bool useMaterial3 = Theme.of(context).useMaterial3; + final bool useMaterial3 = theme.useMaterial3; final double defaultFontSize = buttonStyle.textStyle?.resolve(const {})?.fontSize ?? 14.0; final double effectiveTextScale = diff --git a/packages/material_ui/lib/src/filled_button_theme.dart b/packages/material_ui/lib/src/filled_button_theme.dart index 61db92eae255..8f90452db95a 100644 --- a/packages/material_ui/lib/src/filled_button_theme.dart +++ b/packages/material_ui/lib/src/filled_button_theme.dart @@ -39,7 +39,8 @@ class FilledButtonThemeData with Diagnosticable { /// Creates an [FilledButtonThemeData]. /// /// The [style] may be null. - const FilledButtonThemeData({this.style}); + const FilledButtonThemeData({this.style, this.variant}) + : assert(variant != .material3Expressive, 'Only material3 is supported.'); /// Overrides for [FilledButton]'s default style. /// @@ -50,16 +51,22 @@ class FilledButtonThemeData with Diagnosticable { /// If [style] is null, then this theme doesn't override anything. final ButtonStyle? style; + /// The style variant of Material Design used by [FilledButton]. + final StyleVariant? variant; + /// Linearly interpolate between two filled button themes. static FilledButtonThemeData? lerp(FilledButtonThemeData? a, FilledButtonThemeData? b, double t) { if (identical(a, b)) { return a; } - return FilledButtonThemeData(style: ButtonStyle.lerp(a?.style, b?.style, t)); + return FilledButtonThemeData( + style: ButtonStyle.lerp(a?.style, b?.style, t), + variant: t < 0.5 ? a?.variant : b?.variant, + ); } @override - int get hashCode => style.hashCode; + int get hashCode => Object.hash(style, variant); @override bool operator ==(Object other) { @@ -69,13 +76,14 @@ class FilledButtonThemeData with Diagnosticable { if (other.runtimeType != runtimeType) { return false; } - return other is FilledButtonThemeData && other.style == style; + return other is FilledButtonThemeData && other.style == style && other.variant == variant; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('style', style, defaultValue: null)); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/floating_action_button.dart b/packages/material_ui/lib/src/floating_action_button.dart index b77189cd123b..a50be7419749 100644 --- a/packages/material_ui/lib/src/floating_action_button.dart +++ b/packages/material_ui/lib/src/floating_action_button.dart @@ -491,6 +491,8 @@ class FloatingActionButton extends StatelessWidget { final FloatingActionButtonThemeData floatingActionButtonTheme = FloatingActionButtonTheme.of( context, ); + final StyleVariant effectiveVariant = floatingActionButtonTheme.variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); final FloatingActionButtonThemeData defaults = theme.useMaterial3 ? _FABDefaultsM3(context, _floatingActionButtonType, child != null) : _FABDefaultsM2(context, _floatingActionButtonType, child != null); diff --git a/packages/material_ui/lib/src/floating_action_button_theme.dart b/packages/material_ui/lib/src/floating_action_button_theme.dart index 5abfc47b49ee..04f942ac147b 100644 --- a/packages/material_ui/lib/src/floating_action_button_theme.dart +++ b/packages/material_ui/lib/src/floating_action_button_theme.dart @@ -64,7 +64,11 @@ class FloatingActionButtonThemeData with Diagnosticable { this.extendedPadding, this.extendedTextStyle, this.mouseCursor, - }); + this.variant, + }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); + + /// The style variant of Material Design used by [FloatingActionButton]. + final StyleVariant? variant; /// Color to be used for the unselected, enabled [FloatingActionButton]'s /// foreground. @@ -171,6 +175,7 @@ class FloatingActionButtonThemeData with Diagnosticable { EdgeInsetsGeometry? extendedPadding, TextStyle? extendedTextStyle, WidgetStateProperty? mouseCursor, + StyleVariant? variant, }) { return FloatingActionButtonThemeData( foregroundColor: foregroundColor ?? this.foregroundColor, @@ -194,6 +199,7 @@ class FloatingActionButtonThemeData with Diagnosticable { extendedPadding: extendedPadding ?? this.extendedPadding, extendedTextStyle: extendedTextStyle ?? this.extendedTextStyle, mouseCursor: mouseCursor ?? this.mouseCursor, + variant: variant ?? this.variant, ); } @@ -248,6 +254,7 @@ class FloatingActionButtonThemeData with Diagnosticable { extendedPadding: EdgeInsetsGeometry.lerp(a?.extendedPadding, b?.extendedPadding, t), extendedTextStyle: TextStyle.lerp(a?.extendedTextStyle, b?.extendedTextStyle, t), mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor, + variant: t < 0.5 ? a?.variant : b?.variant, ); } @@ -272,7 +279,7 @@ class FloatingActionButtonThemeData with Diagnosticable { extendedSizeConstraints, extendedIconLabelSpacing, extendedPadding, - Object.hash(extendedTextStyle, mouseCursor), + Object.hash(extendedTextStyle, mouseCursor, variant), ); @override @@ -304,7 +311,8 @@ class FloatingActionButtonThemeData with Diagnosticable { other.extendedIconLabelSpacing == extendedIconLabelSpacing && other.extendedPadding == extendedPadding && other.extendedTextStyle == extendedTextStyle && - other.mouseCursor == mouseCursor; + other.mouseCursor == mouseCursor && + other.variant == variant; } @override @@ -368,6 +376,7 @@ class FloatingActionButtonThemeData with Diagnosticable { defaultValue: null, ), ); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/icon_button.dart b/packages/material_ui/lib/src/icon_button.dart index 9f026b7aa3dc..57ab7b0fcf7d 100644 --- a/packages/material_ui/lib/src/icon_button.dart +++ b/packages/material_ui/lib/src/icon_button.dart @@ -718,6 +718,9 @@ class IconButton extends StatelessWidget { final ThemeData theme = Theme.of(context); if (theme.useMaterial3) { + final StyleVariant effectiveVariant = IconButtonTheme.of(context).variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + final Size? minSize = constraints == null ? null : Size(constraints!.minWidth, constraints!.minHeight); diff --git a/packages/material_ui/lib/src/icon_button_theme.dart b/packages/material_ui/lib/src/icon_button_theme.dart index 0d38686dba0f..dff867b73a02 100644 --- a/packages/material_ui/lib/src/icon_button_theme.dart +++ b/packages/material_ui/lib/src/icon_button_theme.dart @@ -39,7 +39,8 @@ class IconButtonThemeData with Diagnosticable { /// Creates a [IconButtonThemeData]. /// /// The [style] may be null. - const IconButtonThemeData({this.style}); + const IconButtonThemeData({this.style, this.variant}) + : assert(variant != .material3Expressive, 'Only material3 is supported.'); /// Overrides for [IconButton]'s default style if [ThemeData.useMaterial3] /// is set to true. @@ -50,16 +51,22 @@ class IconButtonThemeData with Diagnosticable { /// If [style] is null, then this theme doesn't override anything. final ButtonStyle? style; + /// The style variant of Material Design used by [IconButton]. + final StyleVariant? variant; + /// Linearly interpolate between two icon button themes. static IconButtonThemeData? lerp(IconButtonThemeData? a, IconButtonThemeData? b, double t) { if (identical(a, b)) { return a; } - return IconButtonThemeData(style: ButtonStyle.lerp(a?.style, b?.style, t)); + return IconButtonThemeData( + style: ButtonStyle.lerp(a?.style, b?.style, t), + variant: t < 0.5 ? a?.variant : b?.variant, + ); } @override - int get hashCode => style.hashCode; + int get hashCode => Object.hash(style, variant); @override bool operator ==(Object other) { @@ -69,13 +76,14 @@ class IconButtonThemeData with Diagnosticable { if (other.runtimeType != runtimeType) { return false; } - return other is IconButtonThemeData && other.style == style; + return other is IconButtonThemeData && other.style == style && other.variant == variant; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('style', style, defaultValue: null)); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/menu_anchor.dart b/packages/material_ui/lib/src/menu_anchor.dart index 1126c8c2e03a..7b64da96d655 100644 --- a/packages/material_ui/lib/src/menu_anchor.dart +++ b/packages/material_ui/lib/src/menu_anchor.dart @@ -665,6 +665,11 @@ class _MenuAnchorState extends State with SingleTickerProviderStateM @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final MenuThemeData menuTheme = MenuTheme.of(context); + final StyleVariant effectiveVariant = menuTheme.variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + final Widget child = _MenuAnchorScope( state: this, animationStatus: _animationController.status, @@ -1228,6 +1233,11 @@ class _MenuItemButtonState extends State { @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final MenuButtonThemeData menuButtonTheme = MenuButtonTheme.of(context); + final StyleVariant effectiveVariant = menuButtonTheme.variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + // Since we don't want to use the theme style or default style from the // TextButton, we merge the styles, merging them in the right order when // each type of style exists. Each "*StyleOf" function is only called once. @@ -2108,6 +2118,11 @@ class _SubmenuButtonState extends State { @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final MenuButtonThemeData menuButtonTheme = MenuButtonTheme.of(context); + final StyleVariant effectiveVariant = menuButtonTheme.variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + Offset menuPaddingOffset = widget.alignmentOffset ?? Offset.zero; final EdgeInsets menuPadding = _computeMenuPadding(context); final Axis orientation = _parent?._orientation ?? Axis.vertical; diff --git a/packages/material_ui/lib/src/menu_button_theme.dart b/packages/material_ui/lib/src/menu_button_theme.dart index 11e4b2db72dc..9c25936daac1 100644 --- a/packages/material_ui/lib/src/menu_button_theme.dart +++ b/packages/material_ui/lib/src/menu_button_theme.dart @@ -53,7 +53,8 @@ class MenuButtonThemeData with Diagnosticable { /// Creates a [MenuButtonThemeData]. /// /// The [style] may be null. - const MenuButtonThemeData({this.style}); + const MenuButtonThemeData({this.style, this.variant}) + : assert(variant != .material3Expressive, 'Only material3 is supported.'); /// Overrides for [SubmenuButton] and [MenuItemButton]'s default style. /// @@ -64,16 +65,22 @@ class MenuButtonThemeData with Diagnosticable { /// If [style] is null, then this theme doesn't override anything. final ButtonStyle? style; + /// The style variant of Material Design used by menu buttons. + final StyleVariant? variant; + /// Linearly interpolate between two menu button themes. static MenuButtonThemeData? lerp(MenuButtonThemeData? a, MenuButtonThemeData? b, double t) { if (identical(a, b)) { return a; } - return MenuButtonThemeData(style: ButtonStyle.lerp(a?.style, b?.style, t)); + return MenuButtonThemeData( + style: ButtonStyle.lerp(a?.style, b?.style, t), + variant: t < 0.5 ? a?.variant : b?.variant, + ); } @override - int get hashCode => style.hashCode; + int get hashCode => Object.hash(style, variant); @override bool operator ==(Object other) { @@ -83,13 +90,14 @@ class MenuButtonThemeData with Diagnosticable { if (other.runtimeType != runtimeType) { return false; } - return other is MenuButtonThemeData && other.style == style; + return other is MenuButtonThemeData && other.style == style && other.variant == variant; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('style', style, defaultValue: null)); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/menu_theme.dart b/packages/material_ui/lib/src/menu_theme.dart index c5d1733d0030..5c0509b79f07 100644 --- a/packages/material_ui/lib/src/menu_theme.dart +++ b/packages/material_ui/lib/src/menu_theme.dart @@ -37,7 +37,8 @@ import 'theme.dart'; @immutable class MenuThemeData with Diagnosticable { /// Creates a const set of properties used to configure [MenuTheme]. - const MenuThemeData({this.style, this.submenuIcon}); + const MenuThemeData({this.style, this.submenuIcon, this.variant}) + : assert(variant != .material3Expressive, 'Only material3 is supported.'); /// The [MenuStyle] of a [SubmenuButton] menu. /// @@ -53,6 +54,9 @@ class MenuThemeData with Diagnosticable { /// * [WidgetState.focused]. final WidgetStateProperty? submenuIcon; + /// The style variant of Material Design used by menus. + final StyleVariant? variant; + /// Linearly interpolate between two menu button themes. static MenuThemeData? lerp(MenuThemeData? a, MenuThemeData? b, double t) { if (identical(a, b)) { @@ -61,11 +65,12 @@ class MenuThemeData with Diagnosticable { return MenuThemeData( style: MenuStyle.lerp(a?.style, b?.style, t), submenuIcon: t < 0.5 ? a?.submenuIcon : b?.submenuIcon, + variant: t < 0.5 ? a?.variant : b?.variant, ); } @override - int get hashCode => Object.hash(style, submenuIcon); + int get hashCode => Object.hash(style, submenuIcon, variant); @override bool operator ==(Object other) { @@ -75,7 +80,10 @@ class MenuThemeData with Diagnosticable { if (other.runtimeType != runtimeType) { return false; } - return other is MenuThemeData && other.style == style && other.submenuIcon == submenuIcon; + return other is MenuThemeData && + other.style == style && + other.submenuIcon == submenuIcon && + other.variant == variant; } @override @@ -89,6 +97,7 @@ class MenuThemeData with Diagnosticable { defaultValue: null, ), ); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/navigation_bar.dart b/packages/material_ui/lib/src/navigation_bar.dart index d8420a1bda27..9e042a1a28dd 100644 --- a/packages/material_ui/lib/src/navigation_bar.dart +++ b/packages/material_ui/lib/src/navigation_bar.dart @@ -275,9 +275,12 @@ class NavigationBar extends StatelessWidget { @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); final NavigationBarThemeData defaults = _defaultsFor(context); final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context); + final StyleVariant effectiveVariant = navigationBarTheme.variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); final double effectiveHeight = height ?? navigationBarTheme.height ?? defaults.height!; final NavigationDestinationLabelBehavior effectiveLabelBehavior = labelBehavior ?? navigationBarTheme.labelBehavior ?? defaults.labelBehavior!; diff --git a/packages/material_ui/lib/src/navigation_bar_theme.dart b/packages/material_ui/lib/src/navigation_bar_theme.dart index 3123f6727410..c4608147ff9f 100644 --- a/packages/material_ui/lib/src/navigation_bar_theme.dart +++ b/packages/material_ui/lib/src/navigation_bar_theme.dart @@ -52,7 +52,8 @@ class NavigationBarThemeData with Diagnosticable { this.labelBehavior, this.overlayColor, this.labelPadding, - }); + this.variant, + }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); /// Overrides the default value of [NavigationBar.height]. final double? height; @@ -97,6 +98,9 @@ class NavigationBarThemeData with Diagnosticable { /// Overrides the default value of [NavigationBar.labelPadding]. final EdgeInsetsGeometry? labelPadding; + /// The style variant of Material Design used by [NavigationBar]. + final StyleVariant? variant; + /// Creates a copy of this object with the given fields replaced with the /// new values. NavigationBarThemeData copyWith({ @@ -112,6 +116,7 @@ class NavigationBarThemeData with Diagnosticable { NavigationDestinationLabelBehavior? labelBehavior, WidgetStateProperty? overlayColor, EdgeInsetsGeometry? labelPadding, + StyleVariant? variant, }) { return NavigationBarThemeData( height: height ?? this.height, @@ -126,6 +131,7 @@ class NavigationBarThemeData with Diagnosticable { labelBehavior: labelBehavior ?? this.labelBehavior, overlayColor: overlayColor ?? this.overlayColor, labelPadding: labelPadding ?? this.labelPadding, + variant: variant ?? this.variant, ); } @@ -170,6 +176,7 @@ class NavigationBarThemeData with Diagnosticable { Color.lerp, ), labelPadding: EdgeInsetsGeometry.lerp(a?.labelPadding, b?.labelPadding, t), + variant: t < 0.5 ? a?.variant : b?.variant, ); } @@ -187,6 +194,7 @@ class NavigationBarThemeData with Diagnosticable { labelBehavior, overlayColor, labelPadding, + variant, ); @override @@ -209,7 +217,8 @@ class NavigationBarThemeData with Diagnosticable { other.iconTheme == iconTheme && other.labelBehavior == labelBehavior && other.overlayColor == overlayColor && - other.labelPadding == labelPadding; + other.labelPadding == labelPadding && + other.variant == variant; } @override @@ -255,6 +264,7 @@ class NavigationBarThemeData with Diagnosticable { properties.add( DiagnosticsProperty('labelPadding', labelPadding, defaultValue: null), ); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/navigation_rail.dart b/packages/material_ui/lib/src/navigation_rail.dart index 20b0ba29d5ed..e09739e221c0 100644 --- a/packages/material_ui/lib/src/navigation_rail.dart +++ b/packages/material_ui/lib/src/navigation_rail.dart @@ -444,8 +444,11 @@ class _NavigationRailState extends State with TickerProviderStat @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); final NavigationRailThemeData navigationRailTheme = NavigationRailTheme.of(context); - final NavigationRailThemeData defaults = Theme.of(context).useMaterial3 + final StyleVariant effectiveVariant = navigationRailTheme.variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + final NavigationRailThemeData defaults = theme.useMaterial3 ? _NavigationRailDefaultsM3(context) : _NavigationRailDefaultsM2(context); final MaterialLocalizations localizations = MaterialLocalizations.of(context); @@ -489,7 +492,7 @@ class _NavigationRailState extends State with TickerProviderStat // For backwards compatibility, in M2 the opacity of the unselected icons needs // to be set to the default if it isn't in the given theme. This can be removed // when Material 3 is the default. - final IconThemeData effectiveUnselectedIconTheme = Theme.of(context).useMaterial3 + final IconThemeData effectiveUnselectedIconTheme = theme.useMaterial3 ? unselectedIconTheme : unselectedIconTheme.copyWith( opacity: unselectedIconTheme.opacity ?? defaults.unselectedIconTheme!.opacity, diff --git a/packages/material_ui/lib/src/navigation_rail_theme.dart b/packages/material_ui/lib/src/navigation_rail_theme.dart index 4e3308446691..36d6f6a9cbdc 100644 --- a/packages/material_ui/lib/src/navigation_rail_theme.dart +++ b/packages/material_ui/lib/src/navigation_rail_theme.dart @@ -54,7 +54,8 @@ class NavigationRailThemeData with Diagnosticable { this.indicatorShape, this.minWidth, this.minExtendedWidth, - }); + this.variant, + }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); /// Color to be used for the [NavigationRail]'s background. final Color? backgroundColor; @@ -105,6 +106,9 @@ class NavigationRailThemeData with Diagnosticable { /// is extended. final double? minExtendedWidth; + /// The style variant of Material Design used by [NavigationRail]. + final StyleVariant? variant; + /// Creates a copy of this object with the given fields replaced with the /// new values. NavigationRailThemeData copyWith({ @@ -121,6 +125,7 @@ class NavigationRailThemeData with Diagnosticable { ShapeBorder? indicatorShape, double? minWidth, double? minExtendedWidth, + StyleVariant? variant, }) { return NavigationRailThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, @@ -136,6 +141,7 @@ class NavigationRailThemeData with Diagnosticable { indicatorShape: indicatorShape ?? this.indicatorShape, minWidth: minWidth ?? this.minWidth, minExtendedWidth: minExtendedWidth ?? this.minExtendedWidth, + variant: variant ?? this.variant, ); } @@ -178,6 +184,7 @@ class NavigationRailThemeData with Diagnosticable { indicatorShape: ShapeBorder.lerp(a?.indicatorShape, b?.indicatorShape, t), minWidth: lerpDouble(a?.minWidth, b?.minWidth, t), minExtendedWidth: lerpDouble(a?.minExtendedWidth, b?.minExtendedWidth, t), + variant: t < 0.5 ? a?.variant : b?.variant, ); } @@ -196,6 +203,7 @@ class NavigationRailThemeData with Diagnosticable { indicatorShape, minWidth, minExtendedWidth, + variant, ); @override @@ -219,7 +227,8 @@ class NavigationRailThemeData with Diagnosticable { other.indicatorColor == indicatorColor && other.indicatorShape == indicatorShape && other.minWidth == minWidth && - other.minExtendedWidth == minExtendedWidth; + other.minExtendedWidth == minExtendedWidth && + other.variant == variant; } @override @@ -290,6 +299,7 @@ class NavigationRailThemeData with Diagnosticable { defaultValue: defaultData.minExtendedWidth, ), ); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/outlined_button.dart b/packages/material_ui/lib/src/outlined_button.dart index 6ae8aa6f12b2..456f45c2f86a 100644 --- a/packages/material_ui/lib/src/outlined_button.dart +++ b/packages/material_ui/lib/src/outlined_button.dart @@ -348,6 +348,8 @@ class OutlinedButton extends ButtonStyleButton { @override ButtonStyle defaultStyleOf(BuildContext context) { final ThemeData theme = Theme.of(context); + final StyleVariant effectiveVariant = OutlinedButtonTheme.of(context).variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); final ColorScheme colorScheme = theme.colorScheme; final ButtonStyle buttonStyle = theme.useMaterial3 ? _OutlinedButtonDefaultsM3(context) diff --git a/packages/material_ui/lib/src/outlined_button_theme.dart b/packages/material_ui/lib/src/outlined_button_theme.dart index 1bc14d32d7c6..d84523b61d9b 100644 --- a/packages/material_ui/lib/src/outlined_button_theme.dart +++ b/packages/material_ui/lib/src/outlined_button_theme.dart @@ -39,7 +39,8 @@ class OutlinedButtonThemeData with Diagnosticable { /// Creates a [OutlinedButtonThemeData]. /// /// The [style] may be null. - const OutlinedButtonThemeData({this.style}); + const OutlinedButtonThemeData({this.style, this.variant}) + : assert(variant != .material3Expressive, 'Only material3 is supported.'); /// Overrides for [OutlinedButton]'s default style. /// @@ -50,6 +51,9 @@ class OutlinedButtonThemeData with Diagnosticable { /// If [style] is null, then this theme doesn't override anything. final ButtonStyle? style; + /// The style variant of Material Design used by [OutlinedButton]. + final StyleVariant? variant; + /// Linearly interpolate between two outlined button themes. static OutlinedButtonThemeData? lerp( OutlinedButtonThemeData? a, @@ -59,11 +63,14 @@ class OutlinedButtonThemeData with Diagnosticable { if (identical(a, b)) { return a; } - return OutlinedButtonThemeData(style: ButtonStyle.lerp(a?.style, b?.style, t)); + return OutlinedButtonThemeData( + style: ButtonStyle.lerp(a?.style, b?.style, t), + variant: t < 0.5 ? a?.variant : b?.variant, + ); } @override - int get hashCode => style.hashCode; + int get hashCode => Object.hash(style, variant); @override bool operator ==(Object other) { @@ -73,13 +80,14 @@ class OutlinedButtonThemeData with Diagnosticable { if (other.runtimeType != runtimeType) { return false; } - return other is OutlinedButtonThemeData && other.style == style; + return other is OutlinedButtonThemeData && other.style == style && other.variant == variant; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('style', style, defaultValue: null)); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/progress_indicator.dart b/packages/material_ui/lib/src/progress_indicator.dart index 8e8489d5dc8a..911b83b69f9e 100644 --- a/packages/material_ui/lib/src/progress_indicator.dart +++ b/packages/material_ui/lib/src/progress_indicator.dart @@ -647,6 +647,10 @@ class _LinearProgressIndicatorState extends State @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context); + final StyleVariant effectiveVariant = indicatorTheme.variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); final TextDirection textDirection = Directionality.of(context); if (widget._effectiveValue != null) { @@ -1126,9 +1130,12 @@ class _CircularProgressIndicatorState extends State double offsetValue, double rotationValue, ) { + final ThemeData theme = Theme.of(context); final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context); + final StyleVariant effectiveVariant = indicatorTheme.variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); final bool year2023 = widget.year2023 ?? indicatorTheme.year2023 ?? true; - final ProgressIndicatorThemeData defaults = switch (Theme.of(context).useMaterial3) { + final ProgressIndicatorThemeData defaults = switch (theme.useMaterial3) { true => year2023 ? _CircularProgressIndicatorDefaultsM3Year2023( diff --git a/packages/material_ui/lib/src/progress_indicator_theme.dart b/packages/material_ui/lib/src/progress_indicator_theme.dart index 6be38c4d6ca7..64651caf836f 100644 --- a/packages/material_ui/lib/src/progress_indicator_theme.dart +++ b/packages/material_ui/lib/src/progress_indicator_theme.dart @@ -57,7 +57,8 @@ class ProgressIndicatorThemeData with Diagnosticable { ) this.year2023, this.controller, - }); + this.variant, + }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); /// The color of the [ProgressIndicator]'s indicator. /// @@ -152,6 +153,9 @@ class ProgressIndicatorThemeData with Diagnosticable { /// manage its own internal [AnimationController]. final AnimationController? controller; + /// The style variant of Material Design used by progress indicators. + final StyleVariant? variant; + /// Creates a copy of this object but with the given fields replaced with the /// new values. ProgressIndicatorThemeData copyWith({ @@ -171,6 +175,7 @@ class ProgressIndicatorThemeData with Diagnosticable { EdgeInsetsGeometry? circularTrackPadding, bool? year2023, AnimationController? controller, + StyleVariant? variant, }) { return ProgressIndicatorThemeData( color: color ?? this.color, @@ -189,6 +194,7 @@ class ProgressIndicatorThemeData with Diagnosticable { circularTrackPadding: circularTrackPadding ?? this.circularTrackPadding, year2023: year2023 ?? this.year2023, controller: controller ?? this.controller, + variant: variant ?? this.variant, ); } @@ -224,6 +230,7 @@ class ProgressIndicatorThemeData with Diagnosticable { ), year2023: t < 0.5 ? a?.year2023 : b?.year2023, controller: t < 0.5 ? a?.controller : b?.controller, + variant: t < 0.5 ? a?.variant : b?.variant, ); } @@ -245,6 +252,7 @@ class ProgressIndicatorThemeData with Diagnosticable { circularTrackPadding, year2023, controller, + variant, ); @override @@ -271,7 +279,8 @@ class ProgressIndicatorThemeData with Diagnosticable { other.trackGap == trackGap && other.circularTrackPadding == circularTrackPadding && other.year2023 == year2023 && - other.controller == controller; + other.controller == controller && + other.variant == variant; } @override @@ -307,6 +316,7 @@ class ProgressIndicatorThemeData with Diagnosticable { properties.add( DiagnosticsProperty('controller', controller, defaultValue: null), ); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/range_slider.dart b/packages/material_ui/lib/src/range_slider.dart index fb735a936cc7..ed3e549e6c1d 100644 --- a/packages/material_ui/lib/src/range_slider.dart +++ b/packages/material_ui/lib/src/range_slider.dart @@ -653,6 +653,8 @@ class _RangeSliderState extends State with TickerProviderStateMixin final ThemeData theme = Theme.of(context); SliderThemeData sliderTheme = SliderTheme.of(context); + final StyleVariant effectiveVariant = sliderTheme.variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); final bool year2023 = widget.year2023 ?? sliderTheme.year2023 ?? true; final SliderThemeData defaults = theme.useMaterial3 && !year2023 ? _RangeSliderDefaultsM3(context) diff --git a/packages/material_ui/lib/src/search_anchor.dart b/packages/material_ui/lib/src/search_anchor.dart index 3763971e8faa..daa734b35a5c 100644 --- a/packages/material_ui/lib/src/search_anchor.dart +++ b/packages/material_ui/lib/src/search_anchor.dart @@ -568,6 +568,11 @@ class _SearchAnchorState extends State { @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final SearchViewThemeData viewTheme = SearchViewTheme.of(context); + final StyleVariant effectiveVariant = viewTheme.variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + return AnimatedOpacity( key: _anchorKey, opacity: _getOpacity(), @@ -1661,8 +1666,11 @@ class _SearchBarState extends State { @override Widget build(BuildContext context) { final TextDirection textDirection = Directionality.of(context); - final ColorScheme colorScheme = Theme.of(context).colorScheme; + final ThemeData theme = Theme.of(context); + final ColorScheme colorScheme = theme.colorScheme; final SearchBarThemeData searchBarTheme = SearchBarTheme.of(context); + final StyleVariant effectiveVariant = searchBarTheme.variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); final SearchBarThemeData defaults = _SearchBarDefaultsM3(context); T? resolve( diff --git a/packages/material_ui/lib/src/search_bar_theme.dart b/packages/material_ui/lib/src/search_bar_theme.dart index ddce8adbd15b..eb470a1ef8a4 100644 --- a/packages/material_ui/lib/src/search_bar_theme.dart +++ b/packages/material_ui/lib/src/search_bar_theme.dart @@ -51,7 +51,8 @@ class SearchBarThemeData with Diagnosticable { this.hintStyle, this.constraints, this.textCapitalization, - }); + this.variant, + }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); /// Overrides the default value of the [SearchBar.elevation]. final WidgetStateProperty? elevation; @@ -89,6 +90,9 @@ class SearchBarThemeData with Diagnosticable { /// Overrides the value of [SearchBar.textCapitalization]. final TextCapitalization? textCapitalization; + /// The style variant of Material Design used by [SearchBar]. + final StyleVariant? variant; + /// Creates a copy of this object but with the given fields replaced with the /// new values. SearchBarThemeData copyWith({ @@ -104,6 +108,7 @@ class SearchBarThemeData with Diagnosticable { WidgetStateProperty? hintStyle, BoxConstraints? constraints, TextCapitalization? textCapitalization, + StyleVariant? variant, }) { return SearchBarThemeData( elevation: elevation ?? this.elevation, @@ -118,6 +123,7 @@ class SearchBarThemeData with Diagnosticable { hintStyle: hintStyle ?? this.hintStyle, constraints: constraints ?? this.constraints, textCapitalization: textCapitalization ?? this.textCapitalization, + variant: variant ?? this.variant, ); } @@ -171,6 +177,7 @@ class SearchBarThemeData with Diagnosticable { ), constraints: BoxConstraints.lerp(a?.constraints, b?.constraints, t), textCapitalization: t < 0.5 ? a?.textCapitalization : b?.textCapitalization, + variant: t < 0.5 ? a?.variant : b?.variant, ); } @@ -188,6 +195,7 @@ class SearchBarThemeData with Diagnosticable { hintStyle, constraints, textCapitalization, + variant, ); @override @@ -210,7 +218,8 @@ class SearchBarThemeData with Diagnosticable { other.textStyle == textStyle && other.hintStyle == hintStyle && other.constraints == constraints && - other.textCapitalization == textCapitalization; + other.textCapitalization == textCapitalization && + other.variant == variant; } @override @@ -284,6 +293,7 @@ class SearchBarThemeData with Diagnosticable { defaultValue: null, ), ); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/search_view_theme.dart b/packages/material_ui/lib/src/search_view_theme.dart index 563f85ac3a1c..1ca4a7d39b11 100644 --- a/packages/material_ui/lib/src/search_view_theme.dart +++ b/packages/material_ui/lib/src/search_view_theme.dart @@ -53,7 +53,8 @@ class SearchViewThemeData with Diagnosticable { this.headerTextStyle, this.headerHintStyle, this.dividerColor, - }); + this.variant, + }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); /// Overrides the default value of the [SearchAnchor.viewBackgroundColor]. final Color? backgroundColor; @@ -93,6 +94,9 @@ class SearchViewThemeData with Diagnosticable { /// Overrides the value of the divider color for [SearchAnchor.dividerColor]. final Color? dividerColor; + + /// The style variant of Material Design used by search views. + final StyleVariant? variant; /// Creates a copy of this object but with the given fields replaced with the /// new values. @@ -110,6 +114,7 @@ class SearchViewThemeData with Diagnosticable { EdgeInsetsGeometry? barPadding, bool? shrinkWrap, Color? dividerColor, + StyleVariant? variant, }) { return SearchViewThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, @@ -125,6 +130,7 @@ class SearchViewThemeData with Diagnosticable { barPadding: barPadding ?? this.barPadding, shrinkWrap: shrinkWrap ?? this.shrinkWrap, dividerColor: dividerColor ?? this.dividerColor, + variant: variant ?? this.variant, ); } @@ -147,6 +153,7 @@ class SearchViewThemeData with Diagnosticable { barPadding: EdgeInsetsGeometry.lerp(a?.barPadding, b?.barPadding, t), shrinkWrap: t < 0.5 ? a?.shrinkWrap : b?.shrinkWrap, dividerColor: Color.lerp(a?.dividerColor, b?.dividerColor, t), + variant: t < 0.5 ? a?.variant : b?.variant, ); } @@ -165,6 +172,7 @@ class SearchViewThemeData with Diagnosticable { barPadding, shrinkWrap, dividerColor, + variant, ); @override @@ -188,7 +196,8 @@ class SearchViewThemeData with Diagnosticable { other.padding == padding && other.barPadding == barPadding && other.shrinkWrap == shrinkWrap && - other.dividerColor == dividerColor; + other.dividerColor == dividerColor && + other.variant == variant; } @override @@ -221,6 +230,7 @@ class SearchViewThemeData with Diagnosticable { ); properties.add(DiagnosticsProperty('shrinkWrap', shrinkWrap, defaultValue: null)); properties.add(DiagnosticsProperty('dividerColor', dividerColor, defaultValue: null)); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } // Special case because BorderSide.lerp() doesn't support null arguments diff --git a/packages/material_ui/lib/src/slider.dart b/packages/material_ui/lib/src/slider.dart index 4b99d858f387..26ad239b388e 100644 --- a/packages/material_ui/lib/src/slider.dart +++ b/packages/material_ui/lib/src/slider.dart @@ -831,6 +831,8 @@ class _SliderState extends State with TickerProviderStateMixin { Widget _buildMaterialSlider(BuildContext context) { final ThemeData theme = Theme.of(context); SliderThemeData sliderTheme = SliderTheme.of(context); + final StyleVariant effectiveVariant = sliderTheme.variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); final bool year2023 = widget.year2023 ?? sliderTheme.year2023 ?? true; final SliderThemeData defaults = switch (theme.useMaterial3) { true => year2023 ? _SliderDefaultsM3Year2023(context) : _SliderDefaultsM3(context), diff --git a/packages/material_ui/lib/src/slider_theme.dart b/packages/material_ui/lib/src/slider_theme.dart index 8275d4cad7ff..b1888cb0c845 100644 --- a/packages/material_ui/lib/src/slider_theme.dart +++ b/packages/material_ui/lib/src/slider_theme.dart @@ -318,7 +318,8 @@ class SliderThemeData with Diagnosticable { 'This feature was deprecated after v3.27.0-0.2.pre.', ) this.year2023, - }); + this.variant, + }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); /// Generates a SliderThemeData from three main colors. /// @@ -658,6 +659,9 @@ class SliderThemeData with Diagnosticable { 'This feature was deprecated after v3.27.0-0.2.pre.', ) final bool? year2023; + + /// The style variant of Material Design used by sliders. + final StyleVariant? variant; /// Creates a copy of this object but with the given fields replaced with the /// new values. @@ -698,6 +702,7 @@ class SliderThemeData with Diagnosticable { WidgetStateProperty? thumbSize, double? trackGap, bool? year2023, + StyleVariant? variant, }) { return SliderThemeData( trackHeight: trackHeight ?? this.trackHeight, @@ -738,6 +743,7 @@ class SliderThemeData with Diagnosticable { thumbSize: thumbSize ?? this.thumbSize, trackGap: trackGap ?? this.trackGap, year2023: year2023 ?? this.year2023, + variant: variant ?? this.variant, ); } @@ -821,6 +827,7 @@ class SliderThemeData with Diagnosticable { thumbSize: WidgetStateProperty.lerp(a.thumbSize, b.thumbSize, t, Size.lerp), trackGap: lerpDouble(a.trackGap, b.trackGap, t), year2023: t < 0.5 ? a.year2023 : b.year2023, + variant: t < 0.5 ? a.variant : b.variant, ); } @@ -862,6 +869,7 @@ class SliderThemeData with Diagnosticable { thumbSize, trackGap, year2023, + variant, ), ); @@ -909,7 +917,8 @@ class SliderThemeData with Diagnosticable { other.padding == padding && other.thumbSize == thumbSize && other.trackGap == trackGap && - other.year2023 == year2023; + other.year2023 == year2023 && + other.variant == variant; } @override @@ -1144,6 +1153,7 @@ class SliderThemeData with Diagnosticable { properties.add( DiagnosticsProperty('year2023', year2023, defaultValue: defaultData.year2023), ); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/text_button.dart b/packages/material_ui/lib/src/text_button.dart index f2868f3398c2..2119c715798d 100644 --- a/packages/material_ui/lib/src/text_button.dart +++ b/packages/material_ui/lib/src/text_button.dart @@ -378,6 +378,8 @@ class TextButton extends ButtonStyleButton { @override ButtonStyle defaultStyleOf(BuildContext context) { final ThemeData theme = Theme.of(context); + final StyleVariant effectiveVariant = TextButtonTheme.of(context).variant ?? theme.variant; + assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); final ColorScheme colorScheme = theme.colorScheme; final ButtonStyle buttonStyle = theme.useMaterial3 ? _TextButtonDefaultsM3(context) diff --git a/packages/material_ui/lib/src/text_button_theme.dart b/packages/material_ui/lib/src/text_button_theme.dart index e1a7aefb3cb0..ac25a028f6fa 100644 --- a/packages/material_ui/lib/src/text_button_theme.dart +++ b/packages/material_ui/lib/src/text_button_theme.dart @@ -39,7 +39,8 @@ class TextButtonThemeData with Diagnosticable { /// Creates a [TextButtonThemeData]. /// /// The [style] may be null. - const TextButtonThemeData({this.style}); + const TextButtonThemeData({this.style, this.variant}) + : assert(variant != .material3Expressive, 'Only material3 is supported.'); /// Overrides for [TextButton]'s default style. /// @@ -50,16 +51,22 @@ class TextButtonThemeData with Diagnosticable { /// If [style] is null, then this theme doesn't override anything. final ButtonStyle? style; + /// The style variant of Material Design used by [TextButton]. + final StyleVariant? variant; + /// Linearly interpolate between two text button themes. static TextButtonThemeData? lerp(TextButtonThemeData? a, TextButtonThemeData? b, double t) { if (identical(a, b)) { return a; } - return TextButtonThemeData(style: ButtonStyle.lerp(a?.style, b?.style, t)); + return TextButtonThemeData( + style: ButtonStyle.lerp(a?.style, b?.style, t), + variant: t < 0.5 ? a?.variant : b?.variant, + ); } @override - int get hashCode => style.hashCode; + int get hashCode => Object.hash(style, variant); @override bool operator ==(Object other) { @@ -69,13 +76,14 @@ class TextButtonThemeData with Diagnosticable { if (other.runtimeType != runtimeType) { return false; } - return other is TextButtonThemeData && other.style == style; + return other is TextButtonThemeData && other.style == style && other.variant == variant; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('style', style, defaultValue: null)); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/lib/src/theme.dart b/packages/material_ui/lib/src/theme.dart index b103f725904f..e04e1bdb26b8 100644 --- a/packages/material_ui/lib/src/theme.dart +++ b/packages/material_ui/lib/src/theme.dart @@ -14,7 +14,7 @@ import 'material_localizations.dart'; import 'theme_data.dart'; import 'typography.dart'; -export 'theme_data.dart' show Brightness, MaterialTapTargetSize, ThemeData; +export 'theme_data.dart' show Brightness, MaterialTapTargetSize, StyleVariant, ThemeData; /// The duration over which theme changes animate by default. const Duration kThemeAnimationDuration = Duration(milliseconds: 200); diff --git a/packages/material_ui/lib/src/theme_data.dart b/packages/material_ui/lib/src/theme_data.dart index dcd8fccabd5e..369eb84bdb8f 100644 --- a/packages/material_ui/lib/src/theme_data.dart +++ b/packages/material_ui/lib/src/theme_data.dart @@ -396,6 +396,7 @@ class ThemeData with Diagnosticable { extensions ??= >[]; adaptations ??= >[]; variant ??= StyleVariant.material3; + assert(variant != .material3Expressive, 'Only material3 is supported.'); // TODO(bleroux): Clean this up once the type of `inputDecorationTheme` is changed to `InputDecorationThemeData` if (inputDecorationTheme != null) { if (inputDecorationTheme is InputDecorationTheme) { @@ -819,7 +820,8 @@ class ThemeData with Diagnosticable { 'This feature was deprecated after v3.28.0-1.0.pre.', ) required this.indicatorColor, - }) : // DEPRECATED (newest deprecations at the bottom) + }) : assert(variant != .material3Expressive, 'Only material3 is supported.'), + // DEPRECATED (newest deprecations at the bottom) // should not be `required`, use getter pattern to avoid breakages. _buttonBarTheme = buttonBarTheme, assert(buttonBarTheme != null); @@ -1161,7 +1163,7 @@ class ThemeData with Diagnosticable { /// * [Material 3 specification](https://m3.material.io/). final bool useMaterial3; - /// The Material style variant used by Material components. + /// The style variant of Material Design used by Material components. /// /// Defaults to [StyleVariant.material3]. final StyleVariant variant; From 596b20258e80a13fb792008e53cbd3909a0d4904 Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Thu, 18 Jun 2026 15:39:08 -0700 Subject: [PATCH 3/7] Add tests --- .../material_ui/test/app_bar_theme_test.dart | 28 +++++ .../test/elevated_button_theme_test.dart | 28 +++++ .../test/filled_button_theme_test.dart | 28 +++++ .../floating_action_button_theme_test.dart | 30 ++++++ .../test/icon_button_theme_test.dart | 29 +++++ .../test/menu_button_theme_test.dart | 44 ++++++++ .../material_ui/test/menu_theme_test.dart | 33 ++++++ .../test/navigation_bar_theme_test.dart | 33 ++++++ .../test/navigation_rail_theme_test.dart | 34 ++++++ .../test/outlined_button_theme_test.dart | 28 +++++ .../test/progress_indicator_theme_test.dart | 45 ++++++++ .../test/search_bar_theme_test.dart | 25 +++++ .../test/search_view_theme_test.dart | 35 ++++++ .../material_ui/test/slider_theme_test.dart | 43 ++++++++ .../test/text_button_theme_test.dart | 28 +++++ .../material_ui/test/theme_data_test.dart | 101 ++++++++++++++---- 16 files changed, 573 insertions(+), 19 deletions(-) diff --git a/packages/material_ui/test/app_bar_theme_test.dart b/packages/material_ui/test/app_bar_theme_test.dart index c23152855d4b..4d27d489f728 100644 --- a/packages/material_ui/test/app_bar_theme_test.dart +++ b/packages/material_ui/test/app_bar_theme_test.dart @@ -8,6 +8,21 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _AppBarThemeDataWithExpressiveVariant extends AppBarThemeData { + const _AppBarThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { const appBarTheme = AppBarThemeData( backgroundColor: Color(0xff00ff00), @@ -55,6 +70,19 @@ void main() { expect(identical(AppBarTheme.lerp(data, data, 0.5), data), true); }); + testWidgets('AppBar asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: AppBarTheme( + data: const _AppBarThemeDataWithExpressiveVariant(), + child: AppBar(title: const Text('Title')), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + testWidgets('Material2 - Passing no AppBarTheme returns defaults', (WidgetTester tester) async { final theme = ThemeData(useMaterial3: false); await tester.pumpWidget( diff --git a/packages/material_ui/test/elevated_button_theme_test.dart b/packages/material_ui/test/elevated_button_theme_test.dart index 179c311fb81e..b5a69e1c02c5 100644 --- a/packages/material_ui/test/elevated_button_theme_test.dart +++ b/packages/material_ui/test/elevated_button_theme_test.dart @@ -5,6 +5,21 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _ElevatedButtonThemeDataWithExpressiveVariant extends ElevatedButtonThemeData { + const _ElevatedButtonThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { TextStyle iconStyle(WidgetTester tester, IconData icon) { final RichText iconRichText = tester.widget( @@ -19,6 +34,19 @@ void main() { expect(identical(ElevatedButtonThemeData.lerp(data, data, 0.5), data), true); }); + testWidgets('ElevatedButton asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ElevatedButtonTheme( + data: const _ElevatedButtonThemeDataWithExpressiveVariant(), + child: ElevatedButton(onPressed: () {}, child: const Text('Button')), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + testWidgets('Material3: Passing no ElevatedButtonTheme returns defaults', ( WidgetTester tester, ) async { diff --git a/packages/material_ui/test/filled_button_theme_test.dart b/packages/material_ui/test/filled_button_theme_test.dart index b99eb89e620e..0356a0d7f030 100644 --- a/packages/material_ui/test/filled_button_theme_test.dart +++ b/packages/material_ui/test/filled_button_theme_test.dart @@ -5,6 +5,21 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _FilledButtonThemeDataWithExpressiveVariant extends FilledButtonThemeData { + const _FilledButtonThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { TextStyle iconStyle(WidgetTester tester, IconData icon) { final RichText iconRichText = tester.widget( @@ -19,6 +34,19 @@ void main() { expect(identical(FilledButtonThemeData.lerp(data, data, 0.5), data), true); }); + testWidgets('FilledButton asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: FilledButtonTheme( + data: const _FilledButtonThemeDataWithExpressiveVariant(), + child: FilledButton(onPressed: () {}, child: const Text('Button')), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + testWidgets('Passing no FilledButtonTheme returns defaults', (WidgetTester tester) async { const colorScheme = ColorScheme.light(); await tester.pumpWidget( diff --git a/packages/material_ui/test/floating_action_button_theme_test.dart b/packages/material_ui/test/floating_action_button_theme_test.dart index cb5234fb9bea..a69bc40b4b61 100644 --- a/packages/material_ui/test/floating_action_button_theme_test.dart +++ b/packages/material_ui/test/floating_action_button_theme_test.dart @@ -7,6 +7,21 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _FloatingActionButtonThemeDataWithExpressiveVariant extends FloatingActionButtonThemeData { + const _FloatingActionButtonThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { test('FloatingActionButtonThemeData copyWith, ==, hashCode basics', () { expect(const FloatingActionButtonThemeData(), const FloatingActionButtonThemeData().copyWith()); @@ -22,6 +37,21 @@ void main() { expect(identical(FloatingActionButtonThemeData.lerp(data, data, 0.5), data), true); }); + testWidgets('FloatingActionButton asserts on unsupported style variants', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: FloatingActionButtonTheme( + data: const _FloatingActionButtonThemeDataWithExpressiveVariant(), + child: FloatingActionButton(onPressed: () {}), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + testWidgets( 'Material3: Default values are used when no FloatingActionButton or FloatingActionButtonThemeData properties are specified', (WidgetTester tester) async { diff --git a/packages/material_ui/test/icon_button_theme_test.dart b/packages/material_ui/test/icon_button_theme_test.dart index c271ba758bd4..c6265d18749b 100644 --- a/packages/material_ui/test/icon_button_theme_test.dart +++ b/packages/material_ui/test/icon_button_theme_test.dart @@ -7,6 +7,13 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _IconButtonThemeDataWithExpressiveVariant extends IconButtonThemeData { + const _IconButtonThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + void main() { RenderObject getOverlayColor(WidgetTester tester) { return tester.allRenderObjects.firstWhere( @@ -20,6 +27,28 @@ void main() { expect(identical(IconButtonThemeData.lerp(data, data, 0.5), data), true); }); + testWidgets('IconButton asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: IconButtonTheme( + data: _IconButtonThemeDataWithExpressiveVariant(), + child: IconButton(onPressed: null, icon: Icon(Icons.ac_unit)), + ), + ), + ), + ); + + expect( + tester.takeException(), + isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ), + ); + }); + testWidgets('Passing no IconButtonTheme returns defaults', (WidgetTester tester) async { const colorScheme = ColorScheme.light(); await tester.pumpWidget( diff --git a/packages/material_ui/test/menu_button_theme_test.dart b/packages/material_ui/test/menu_button_theme_test.dart index 119e42b2c32f..dc050ca89db9 100644 --- a/packages/material_ui/test/menu_button_theme_test.dart +++ b/packages/material_ui/test/menu_button_theme_test.dart @@ -5,10 +5,54 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _MenuButtonThemeDataWithExpressiveVariant extends MenuButtonThemeData { + const _MenuButtonThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { test('MenuButtonThemeData lerp special cases', () { expect(MenuButtonThemeData.lerp(null, null, 0), null); const data = MenuButtonThemeData(); expect(identical(MenuButtonThemeData.lerp(data, data, 0.5), data), true); }); + + testWidgets('MenuItemButton asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: MenuButtonTheme( + data: const _MenuButtonThemeDataWithExpressiveVariant(), + child: MenuItemButton(onPressed: () {}, child: const Text('Item')), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + + testWidgets('SubmenuButton asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: MenuButtonTheme( + data: const _MenuButtonThemeDataWithExpressiveVariant(), + child: SubmenuButton( + menuChildren: [MenuItemButton(onPressed: () {}, child: const Text('Item'))], + child: const Text('Submenu'), + ), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); } diff --git a/packages/material_ui/test/menu_theme_test.dart b/packages/material_ui/test/menu_theme_test.dart index d86ff0028092..e04baec96fd8 100644 --- a/packages/material_ui/test/menu_theme_test.dart +++ b/packages/material_ui/test/menu_theme_test.dart @@ -7,6 +7,21 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _MenuThemeDataWithExpressiveVariant extends MenuThemeData { + const _MenuThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { void onPressed(TestMenu item) {} @@ -60,6 +75,24 @@ void main() { expect(menuThemeData.submenuIcon, isNull); }); + testWidgets('MenuAnchor asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: MenuTheme( + data: const _MenuThemeDataWithExpressiveVariant(), + child: MenuAnchor( + menuChildren: [MenuItemButton(onPressed: () {}, child: const Text('Item'))], + builder: (BuildContext context, MenuController controller, Widget? child) { + return TextButton(onPressed: controller.open, child: const Text('Open')); + }, + ), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + testWidgets('Default MenuThemeData debugFillProperties', (WidgetTester tester) async { final builder = DiagnosticPropertiesBuilder(); const MenuThemeData().debugFillProperties(builder); diff --git a/packages/material_ui/test/navigation_bar_theme_test.dart b/packages/material_ui/test/navigation_bar_theme_test.dart index 38a78326660c..503bc2ba0b00 100644 --- a/packages/material_ui/test/navigation_bar_theme_test.dart +++ b/packages/material_ui/test/navigation_bar_theme_test.dart @@ -14,6 +14,21 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _NavigationBarThemeDataWithExpressiveVariant extends NavigationBarThemeData { + const _NavigationBarThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { test('copyWith, ==, hashCode basics', () { expect(const NavigationBarThemeData(), const NavigationBarThemeData().copyWith()); @@ -29,6 +44,24 @@ void main() { expect(identical(NavigationBarThemeData.lerp(data, data, 0.5), data), true); }); + testWidgets('NavigationBar asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: NavigationBarTheme( + data: const _NavigationBarThemeDataWithExpressiveVariant(), + child: NavigationBar( + destinations: const [ + NavigationDestination(icon: Icon(Icons.home), label: 'Home'), + NavigationDestination(icon: Icon(Icons.settings), label: 'Settings'), + ], + ), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + testWidgets('Default debugFillProperties', (WidgetTester tester) async { final builder = DiagnosticPropertiesBuilder(); const NavigationBarThemeData().debugFillProperties(builder); diff --git a/packages/material_ui/test/navigation_rail_theme_test.dart b/packages/material_ui/test/navigation_rail_theme_test.dart index b47494e4d865..ef119e1ef173 100644 --- a/packages/material_ui/test/navigation_rail_theme_test.dart +++ b/packages/material_ui/test/navigation_rail_theme_test.dart @@ -6,7 +6,41 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _NavigationRailThemeDataWithExpressiveVariant extends NavigationRailThemeData { + const _NavigationRailThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { + testWidgets('NavigationRail asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: NavigationRailTheme( + data: const _NavigationRailThemeDataWithExpressiveVariant(), + child: NavigationRail( + selectedIndex: 0, + destinations: const [ + NavigationRailDestination(icon: Icon(Icons.home), label: Text('Home')), + NavigationRailDestination(icon: Icon(Icons.settings), label: Text('Settings')), + ], + ), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + test('copyWith, ==, hashCode basics', () { expect(const NavigationRailThemeData(), const NavigationRailThemeData().copyWith()); expect( diff --git a/packages/material_ui/test/outlined_button_theme_test.dart b/packages/material_ui/test/outlined_button_theme_test.dart index 393b985ebd02..a096ee7791cc 100644 --- a/packages/material_ui/test/outlined_button_theme_test.dart +++ b/packages/material_ui/test/outlined_button_theme_test.dart @@ -5,6 +5,21 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _OutlinedButtonThemeDataWithExpressiveVariant extends OutlinedButtonThemeData { + const _OutlinedButtonThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { TextStyle iconStyle(WidgetTester tester, IconData icon) { final RichText iconRichText = tester.widget( @@ -19,6 +34,19 @@ void main() { expect(identical(OutlinedButtonThemeData.lerp(data, data, 0.5), data), true); }); + testWidgets('OutlinedButton asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: OutlinedButtonTheme( + data: const _OutlinedButtonThemeDataWithExpressiveVariant(), + child: OutlinedButton(onPressed: () {}, child: const Text('Button')), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + testWidgets('Material3: Passing no OutlinedButtonTheme returns defaults', ( WidgetTester tester, ) async { diff --git a/packages/material_ui/test/progress_indicator_theme_test.dart b/packages/material_ui/test/progress_indicator_theme_test.dart index edd24237840d..319831b1a686 100644 --- a/packages/material_ui/test/progress_indicator_theme_test.dart +++ b/packages/material_ui/test/progress_indicator_theme_test.dart @@ -12,6 +12,21 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _ProgressIndicatorThemeDataWithExpressiveVariant extends ProgressIndicatorThemeData { + const _ProgressIndicatorThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { test('ProgressIndicatorThemeData copyWith, ==, hashCode, basics', () { expect(const ProgressIndicatorThemeData(), const ProgressIndicatorThemeData().copyWith()); @@ -27,6 +42,36 @@ void main() { expect(identical(ProgressIndicatorThemeData.lerp(data, data, 0.5), data), true); }); + testWidgets('LinearProgressIndicator asserts on unsupported style variants', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: ProgressIndicatorTheme( + data: _ProgressIndicatorThemeDataWithExpressiveVariant(), + child: LinearProgressIndicator(), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + + testWidgets('CircularProgressIndicator asserts on unsupported style variants', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: ProgressIndicatorTheme( + data: _ProgressIndicatorThemeDataWithExpressiveVariant(), + child: CircularProgressIndicator(), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + testWidgets('ProgressIndicatorThemeData implements debugFillProperties', ( WidgetTester tester, ) async { diff --git a/packages/material_ui/test/search_bar_theme_test.dart b/packages/material_ui/test/search_bar_theme_test.dart index 8db913ad882a..5b94cd71fedb 100644 --- a/packages/material_ui/test/search_bar_theme_test.dart +++ b/packages/material_ui/test/search_bar_theme_test.dart @@ -6,6 +6,21 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _SearchBarThemeDataWithExpressiveVariant extends SearchBarThemeData { + const _SearchBarThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { test('SearchBarThemeData copyWith, ==, hashCode basics', () { expect(const SearchBarThemeData(), const SearchBarThemeData().copyWith()); @@ -18,6 +33,16 @@ void main() { expect(identical(SearchBarThemeData.lerp(data, data, 0.5), data), true); }); + testWidgets('SearchBar asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: SearchBarTheme(data: _SearchBarThemeDataWithExpressiveVariant(), child: SearchBar()), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + test('SearchBarThemeData defaults', () { const themeData = SearchBarThemeData(); expect(themeData.elevation, null); diff --git a/packages/material_ui/test/search_view_theme_test.dart b/packages/material_ui/test/search_view_theme_test.dart index df9ef81c8140..ded4fbf074e7 100644 --- a/packages/material_ui/test/search_view_theme_test.dart +++ b/packages/material_ui/test/search_view_theme_test.dart @@ -6,6 +6,21 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _SearchViewThemeDataWithExpressiveVariant extends SearchViewThemeData { + const _SearchViewThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { test('SearchViewThemeData copyWith, ==, hashCode basics', () { expect(const SearchViewThemeData(), const SearchViewThemeData().copyWith()); @@ -18,6 +33,26 @@ void main() { expect(identical(SearchViewThemeData.lerp(data, data, 0.5), data), true); }); + testWidgets('SearchAnchor asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: SearchViewTheme( + data: const _SearchViewThemeDataWithExpressiveVariant(), + child: SearchAnchor( + builder: (BuildContext context, SearchController controller) { + return TextButton(onPressed: controller.openView, child: const Text('Search')); + }, + suggestionsBuilder: (BuildContext context, SearchController controller) { + return []; + }, + ), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + test('SearchViewThemeData defaults', () { const themeData = SearchViewThemeData(); expect(themeData.backgroundColor, null); diff --git a/packages/material_ui/test/slider_theme_test.dart b/packages/material_ui/test/slider_theme_test.dart index 31882acf6942..2089409a8d55 100644 --- a/packages/material_ui/test/slider_theme_test.dart +++ b/packages/material_ui/test/slider_theme_test.dart @@ -7,6 +7,21 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _SliderThemeDataWithExpressiveVariant extends SliderThemeData { + const _SliderThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { test('SliderThemeData copyWith, ==, hashCode basics', () { expect(const SliderThemeData(), const SliderThemeData().copyWith()); @@ -18,6 +33,34 @@ void main() { expect(identical(SliderThemeData.lerp(data, data, 0.5), data), true); }); + testWidgets('RangeSlider asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: SliderTheme( + data: const _SliderThemeDataWithExpressiveVariant(), + child: Material( + child: RangeSlider(values: const RangeValues(0.25, 0.75), onChanged: (_) {}), + ), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + + testWidgets('Slider asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: SliderTheme( + data: const _SliderThemeDataWithExpressiveVariant(), + child: Material(child: Slider(value: 0.5, onChanged: (_) {})), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + testWidgets('Default SliderThemeData debugFillProperties', (WidgetTester tester) async { final builder = DiagnosticPropertiesBuilder(); const SliderThemeData().debugFillProperties(builder); diff --git a/packages/material_ui/test/text_button_theme_test.dart b/packages/material_ui/test/text_button_theme_test.dart index 83be109a5e4d..94829cbdea65 100644 --- a/packages/material_ui/test/text_button_theme_test.dart +++ b/packages/material_ui/test/text_button_theme_test.dart @@ -5,6 +5,21 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; +class _TextButtonThemeDataWithExpressiveVariant extends TextButtonThemeData { + const _TextButtonThemeDataWithExpressiveVariant(); + + @override + StyleVariant? get variant => .material3Expressive; +} + +Matcher get _throwsUnsupportedStyleVariantAssertion { + return isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ); +} + void main() { TextStyle iconStyle(WidgetTester tester, IconData icon) { final RichText iconRichText = tester.widget( @@ -19,6 +34,19 @@ void main() { expect(identical(TextButtonThemeData.lerp(data, data, 0.5), data), true); }); + testWidgets('TextButton asserts on unsupported style variants', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: TextButtonTheme( + data: const _TextButtonThemeDataWithExpressiveVariant(), + child: TextButton(onPressed: () {}, child: const Text('Button')), + ), + ), + ); + + expect(tester.takeException(), _throwsUnsupportedStyleVariantAssertion); + }); + testWidgets('Material3: Passing no TextButtonTheme returns defaults', ( WidgetTester tester, ) async { diff --git a/packages/material_ui/test/theme_data_test.dart b/packages/material_ui/test/theme_data_test.dart index 1b2520656c8c..f046d3627999 100644 --- a/packages/material_ui/test/theme_data_test.dart +++ b/packages/material_ui/test/theme_data_test.dart @@ -24,41 +24,104 @@ void main() { expect(dawn.primaryColor, Color.lerp(dark.primaryColor, light.primaryColor, 0.25)); }); - test('ThemeData supports style variants', () { + test('ThemeData defaults to Material 3 style variant', () { expect(ThemeData().variant, StyleVariant.material3); + expect(ThemeData.light().variant, StyleVariant.material3); + expect(ThemeData.dark().variant, StyleVariant.material3); + expect(ThemeData.fallback().variant, StyleVariant.material3); + }); + + test('ThemeData asserts on unsupported style variants', () { + Matcher throwsUnsupportedStyleVariantAssertion() { + return throwsA( + isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ), + ); + } + expect( - ThemeData(variant: StyleVariant.material3Expressive).variant, - StyleVariant.material3Expressive, + () => ThemeData(variant: .material3Expressive), + throwsUnsupportedStyleVariantAssertion(), ); expect( - ThemeData.light(variant: StyleVariant.material3Expressive).variant, - StyleVariant.material3Expressive, + () => ThemeData.light(variant: .material3Expressive), + throwsUnsupportedStyleVariantAssertion(), ); expect( - ThemeData.dark(variant: StyleVariant.material3Expressive).variant, - StyleVariant.material3Expressive, + () => ThemeData.dark(variant: .material3Expressive), + throwsUnsupportedStyleVariantAssertion(), ); expect( - ThemeData.fallback(variant: StyleVariant.material3Expressive).variant, - StyleVariant.material3Expressive, + () => ThemeData.fallback(variant: .material3Expressive), + throwsUnsupportedStyleVariantAssertion(), + ); + expect( + () => ThemeData.from(colorScheme: const ColorScheme.light(), variant: .material3Expressive), + throwsUnsupportedStyleVariantAssertion(), ); }); - test('ThemeData.copyWith supports style variants', () { + test('ThemeData.copyWith asserts on unsupported style variants', () { final theme = ThemeData(); - final ThemeData expressiveTheme = theme.copyWith(variant: StyleVariant.material3Expressive); - expect(expressiveTheme.variant, StyleVariant.material3Expressive); - expect(expressiveTheme, isNot(theme)); - expect(expressiveTheme.hashCode, isNot(theme.hashCode)); + expect( + () => theme.copyWith(variant: .material3Expressive), + throwsA( + isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ), + ), + ); + }); + + test('component theme data asserts on unsupported style variants', () { + Matcher throwsUnsupportedStyleVariantAssertion() { + return throwsA( + isA().having( + (AssertionError error) => error.message, + 'message', + 'Only material3 is supported.', + ), + ); + } + + final constructors = [ + () => ThemeData(appBarTheme: AppBarThemeData(variant: .material3Expressive)), + () => ThemeData(elevatedButtonTheme: ElevatedButtonThemeData(variant: .material3Expressive)), + () => ThemeData(filledButtonTheme: FilledButtonThemeData(variant: .material3Expressive)), + () => ThemeData( + floatingActionButtonTheme: FloatingActionButtonThemeData(variant: .material3Expressive), + ), + () => ThemeData(iconButtonTheme: IconButtonThemeData(variant: .material3Expressive)), + () => ThemeData(menuButtonTheme: MenuButtonThemeData(variant: .material3Expressive)), + () => ThemeData(menuTheme: MenuThemeData(variant: .material3Expressive)), + () => ThemeData(navigationBarTheme: NavigationBarThemeData(variant: .material3Expressive)), + () => ThemeData(navigationRailTheme: NavigationRailThemeData(variant: .material3Expressive)), + () => ThemeData(outlinedButtonTheme: OutlinedButtonThemeData(variant: .material3Expressive)), + () => ThemeData( + progressIndicatorTheme: ProgressIndicatorThemeData(variant: .material3Expressive), + ), + () => ThemeData(searchBarTheme: SearchBarThemeData(variant: .material3Expressive)), + () => ThemeData(searchViewTheme: SearchViewThemeData(variant: .material3Expressive)), + () => ThemeData(sliderTheme: SliderThemeData(variant: .material3Expressive)), + () => ThemeData(textButtonTheme: TextButtonThemeData(variant: .material3Expressive)), + ]; + + for (final constructor in constructors) { + expect(constructor, throwsUnsupportedStyleVariantAssertion()); + } }); - test('ThemeData.lerp switches style variants discretely', () { + test('ThemeData.lerp preserves style variants', () { final material3 = ThemeData(variant: StyleVariant.material3); - final expressive = ThemeData(variant: StyleVariant.material3Expressive); - expect(ThemeData.lerp(material3, expressive, 0.25).variant, StyleVariant.material3); - expect(ThemeData.lerp(material3, expressive, 0.75).variant, StyleVariant.material3Expressive); + expect(ThemeData.lerp(material3, material3, 0.25).variant, StyleVariant.material3); + expect(ThemeData.lerp(material3, material3, 0.75).variant, StyleVariant.material3); }); test('ThemeData objects with .styleFrom() members are equal', () { @@ -1457,7 +1520,7 @@ void main() { scrollbarTheme: const ScrollbarThemeData(radius: Radius.circular(10.0)), splashFactory: InkRipple.splashFactory, useMaterial3: true, - variant: StyleVariant.material3Expressive, + variant: StyleVariant.material3, visualDensity: VisualDensity.standard, // COLOR canvasColor: Colors.white, From 8b78064ea63852408360ec26d7cd2c9fc1cec0e7 Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Mon, 22 Jun 2026 13:47:34 -0700 Subject: [PATCH 4/7] Move the assert message to debug.dart --- packages/material_ui/lib/src/app_bar.dart | 2 +- packages/material_ui/lib/src/app_bar_theme.dart | 3 ++- packages/material_ui/lib/src/debug.dart | 4 ++++ packages/material_ui/lib/src/elevated_button.dart | 3 ++- packages/material_ui/lib/src/elevated_button_theme.dart | 3 ++- packages/material_ui/lib/src/filled_button.dart | 3 ++- packages/material_ui/lib/src/filled_button_theme.dart | 3 ++- packages/material_ui/lib/src/floating_action_button.dart | 3 ++- .../material_ui/lib/src/floating_action_button_theme.dart | 3 ++- packages/material_ui/lib/src/icon_button.dart | 2 +- packages/material_ui/lib/src/icon_button_theme.dart | 3 ++- packages/material_ui/lib/src/menu_anchor.dart | 7 ++++--- packages/material_ui/lib/src/menu_button_theme.dart | 3 ++- packages/material_ui/lib/src/menu_theme.dart | 3 ++- packages/material_ui/lib/src/navigation_bar.dart | 3 ++- packages/material_ui/lib/src/navigation_bar_theme.dart | 3 ++- packages/material_ui/lib/src/navigation_rail.dart | 3 ++- packages/material_ui/lib/src/navigation_rail_theme.dart | 3 ++- packages/material_ui/lib/src/outlined_button.dart | 3 ++- packages/material_ui/lib/src/outlined_button_theme.dart | 3 ++- packages/material_ui/lib/src/progress_indicator.dart | 5 +++-- packages/material_ui/lib/src/progress_indicator_theme.dart | 3 ++- packages/material_ui/lib/src/range_slider.dart | 2 +- packages/material_ui/lib/src/search_anchor.dart | 5 +++-- packages/material_ui/lib/src/search_bar_theme.dart | 3 ++- packages/material_ui/lib/src/search_view_theme.dart | 5 +++-- packages/material_ui/lib/src/slider.dart | 2 +- packages/material_ui/lib/src/slider_theme.dart | 5 +++-- packages/material_ui/lib/src/text_button.dart | 3 ++- packages/material_ui/lib/src/text_button_theme.dart | 3 ++- packages/material_ui/lib/src/theme_data.dart | 5 +++-- 31 files changed, 67 insertions(+), 37 deletions(-) diff --git a/packages/material_ui/lib/src/app_bar.dart b/packages/material_ui/lib/src/app_bar.dart index 96f6996605fa..cb05c7ee90c2 100644 --- a/packages/material_ui/lib/src/app_bar.dart +++ b/packages/material_ui/lib/src/app_bar.dart @@ -907,7 +907,7 @@ class _AppBarState extends State { final IconButtonThemeData iconButtonTheme = IconButtonTheme.of(context); final AppBarThemeData appBarTheme = AppBarTheme.of(context); final StyleVariant effectiveVariant = appBarTheme.variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final AppBarThemeData defaults = theme.useMaterial3 ? _AppBarDefaultsM3(context) : _AppBarDefaultsM2(context); diff --git a/packages/material_ui/lib/src/app_bar_theme.dart b/packages/material_ui/lib/src/app_bar_theme.dart index 59d502fd3803..877d3c8a416c 100644 --- a/packages/material_ui/lib/src/app_bar_theme.dart +++ b/packages/material_ui/lib/src/app_bar_theme.dart @@ -11,6 +11,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'debug.dart'; import 'theme.dart'; // Examples can assume: @@ -415,7 +416,7 @@ class AppBarThemeData with Diagnosticable { color == null || backgroundColor == null, 'The color and backgroundColor parameters mean the same thing. Only specify one.', ), - assert(variant != .material3Expressive, 'Only material3 is supported.'); + assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// Overrides the default value of [AppBar.backgroundColor]. final Color? backgroundColor; diff --git a/packages/material_ui/lib/src/debug.dart b/packages/material_ui/lib/src/debug.dart index d11299d43337..6e29e543273d 100644 --- a/packages/material_ui/lib/src/debug.dart +++ b/packages/material_ui/lib/src/debug.dart @@ -11,6 +11,10 @@ import 'scaffold.dart' show Scaffold, ScaffoldMessenger; // Examples can assume: // late BuildContext context; +/// The assertion message used while Material components do not yet support +/// Material 3 Expressive. +const String kUnsupportedStyleVariantAssertionMessage = 'Only material3 is supported.'; + /// Asserts that the given context has a [Material] ancestor within the closest /// [LookupBoundary]. /// diff --git a/packages/material_ui/lib/src/elevated_button.dart b/packages/material_ui/lib/src/elevated_button.dart index 471082f5a481..3f02044c7562 100644 --- a/packages/material_ui/lib/src/elevated_button.dart +++ b/packages/material_ui/lib/src/elevated_button.dart @@ -18,6 +18,7 @@ import 'button_style_button.dart'; import 'color_scheme.dart'; import 'colors.dart'; import 'constants.dart'; +import 'debug.dart'; import 'elevated_button_theme.dart'; import 'ink_ripple.dart'; import 'ink_well.dart'; @@ -393,7 +394,7 @@ class ElevatedButton extends ButtonStyleButton { ButtonStyle defaultStyleOf(BuildContext context) { final ThemeData theme = Theme.of(context); final StyleVariant effectiveVariant = ElevatedButtonTheme.of(context).variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final ColorScheme colorScheme = theme.colorScheme; final ButtonStyle buttonStyle = theme.useMaterial3 ? _ElevatedButtonDefaultsM3(context) diff --git a/packages/material_ui/lib/src/elevated_button_theme.dart b/packages/material_ui/lib/src/elevated_button_theme.dart index 490226c2ee06..8851720e9887 100644 --- a/packages/material_ui/lib/src/elevated_button_theme.dart +++ b/packages/material_ui/lib/src/elevated_button_theme.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'button_style.dart'; +import 'debug.dart'; import 'theme.dart'; // Examples can assume: @@ -40,7 +41,7 @@ class ElevatedButtonThemeData with Diagnosticable { /// /// The [style] may be null. const ElevatedButtonThemeData({this.style, this.variant}) - : assert(variant != .material3Expressive, 'Only material3 is supported.'); + : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// Overrides for [ElevatedButton]'s default style. /// diff --git a/packages/material_ui/lib/src/filled_button.dart b/packages/material_ui/lib/src/filled_button.dart index bcaf9493341b..63d4d959c137 100644 --- a/packages/material_ui/lib/src/filled_button.dart +++ b/packages/material_ui/lib/src/filled_button.dart @@ -19,6 +19,7 @@ import 'button_style_button.dart'; import 'color_scheme.dart'; import 'colors.dart'; import 'constants.dart'; +import 'debug.dart'; import 'filled_button_theme.dart'; import 'ink_well.dart'; import 'material_state.dart'; @@ -432,7 +433,7 @@ class FilledButton extends ButtonStyleButton { ButtonStyle defaultStyleOf(BuildContext context) { final ThemeData theme = Theme.of(context); final StyleVariant effectiveVariant = FilledButtonTheme.of(context).variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final ButtonStyle buttonStyle = switch (_variant) { _FilledButtonVariant.filled => _FilledButtonDefaultsM3(context), _FilledButtonVariant.tonal => _FilledTonalButtonDefaultsM3(context), diff --git a/packages/material_ui/lib/src/filled_button_theme.dart b/packages/material_ui/lib/src/filled_button_theme.dart index 8f90452db95a..e648e635b568 100644 --- a/packages/material_ui/lib/src/filled_button_theme.dart +++ b/packages/material_ui/lib/src/filled_button_theme.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'button_style.dart'; +import 'debug.dart'; import 'theme.dart'; // Examples can assume: @@ -40,7 +41,7 @@ class FilledButtonThemeData with Diagnosticable { /// /// The [style] may be null. const FilledButtonThemeData({this.style, this.variant}) - : assert(variant != .material3Expressive, 'Only material3 is supported.'); + : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// Overrides for [FilledButton]'s default style. /// diff --git a/packages/material_ui/lib/src/floating_action_button.dart b/packages/material_ui/lib/src/floating_action_button.dart index a50be7419749..d7d948d5b8ec 100644 --- a/packages/material_ui/lib/src/floating_action_button.dart +++ b/packages/material_ui/lib/src/floating_action_button.dart @@ -15,6 +15,7 @@ import 'package:flutter/widgets.dart'; import 'button.dart'; import 'color_scheme.dart'; +import 'debug.dart'; import 'floating_action_button_theme.dart'; import 'scaffold.dart'; import 'text_theme.dart'; @@ -492,7 +493,7 @@ class FloatingActionButton extends StatelessWidget { context, ); final StyleVariant effectiveVariant = floatingActionButtonTheme.variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final FloatingActionButtonThemeData defaults = theme.useMaterial3 ? _FABDefaultsM3(context, _floatingActionButtonType, child != null) : _FABDefaultsM2(context, _floatingActionButtonType, child != null); diff --git a/packages/material_ui/lib/src/floating_action_button_theme.dart b/packages/material_ui/lib/src/floating_action_button_theme.dart index 04f942ac147b..cb3ad170c1f7 100644 --- a/packages/material_ui/lib/src/floating_action_button_theme.dart +++ b/packages/material_ui/lib/src/floating_action_button_theme.dart @@ -15,6 +15,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'debug.dart'; import 'theme.dart'; // Examples can assume: @@ -65,7 +66,7 @@ class FloatingActionButtonThemeData with Diagnosticable { this.extendedTextStyle, this.mouseCursor, this.variant, - }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); + }) : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// The style variant of Material Design used by [FloatingActionButton]. final StyleVariant? variant; diff --git a/packages/material_ui/lib/src/icon_button.dart b/packages/material_ui/lib/src/icon_button.dart index 57ab7b0fcf7d..d093d3686467 100644 --- a/packages/material_ui/lib/src/icon_button.dart +++ b/packages/material_ui/lib/src/icon_button.dart @@ -719,7 +719,7 @@ class IconButton extends StatelessWidget { if (theme.useMaterial3) { final StyleVariant effectiveVariant = IconButtonTheme.of(context).variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final Size? minSize = constraints == null ? null diff --git a/packages/material_ui/lib/src/icon_button_theme.dart b/packages/material_ui/lib/src/icon_button_theme.dart index dff867b73a02..e20a5d27be3c 100644 --- a/packages/material_ui/lib/src/icon_button_theme.dart +++ b/packages/material_ui/lib/src/icon_button_theme.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'button_style.dart'; +import 'debug.dart'; import 'theme.dart'; // Examples can assume: @@ -40,7 +41,7 @@ class IconButtonThemeData with Diagnosticable { /// /// The [style] may be null. const IconButtonThemeData({this.style, this.variant}) - : assert(variant != .material3Expressive, 'Only material3 is supported.'); + : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// Overrides for [IconButton]'s default style if [ThemeData.useMaterial3] /// is set to true. diff --git a/packages/material_ui/lib/src/menu_anchor.dart b/packages/material_ui/lib/src/menu_anchor.dart index 7b64da96d655..cdb8525076ab 100644 --- a/packages/material_ui/lib/src/menu_anchor.dart +++ b/packages/material_ui/lib/src/menu_anchor.dart @@ -26,6 +26,7 @@ import 'checkbox.dart'; import 'color_scheme.dart'; import 'colors.dart'; import 'constants.dart'; +import 'debug.dart'; import 'icons.dart'; import 'ink_well.dart'; import 'material.dart'; @@ -668,7 +669,7 @@ class _MenuAnchorState extends State with SingleTickerProviderStateM final ThemeData theme = Theme.of(context); final MenuThemeData menuTheme = MenuTheme.of(context); final StyleVariant effectiveVariant = menuTheme.variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final Widget child = _MenuAnchorScope( state: this, @@ -1236,7 +1237,7 @@ class _MenuItemButtonState extends State { final ThemeData theme = Theme.of(context); final MenuButtonThemeData menuButtonTheme = MenuButtonTheme.of(context); final StyleVariant effectiveVariant = menuButtonTheme.variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); // Since we don't want to use the theme style or default style from the // TextButton, we merge the styles, merging them in the right order when @@ -2121,7 +2122,7 @@ class _SubmenuButtonState extends State { final ThemeData theme = Theme.of(context); final MenuButtonThemeData menuButtonTheme = MenuButtonTheme.of(context); final StyleVariant effectiveVariant = menuButtonTheme.variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); Offset menuPaddingOffset = widget.alignmentOffset ?? Offset.zero; final EdgeInsets menuPadding = _computeMenuPadding(context); diff --git a/packages/material_ui/lib/src/menu_button_theme.dart b/packages/material_ui/lib/src/menu_button_theme.dart index 9c25936daac1..da45c6730607 100644 --- a/packages/material_ui/lib/src/menu_button_theme.dart +++ b/packages/material_ui/lib/src/menu_button_theme.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'button_style.dart'; +import 'debug.dart'; import 'menu_anchor.dart'; import 'theme.dart'; @@ -54,7 +55,7 @@ class MenuButtonThemeData with Diagnosticable { /// /// The [style] may be null. const MenuButtonThemeData({this.style, this.variant}) - : assert(variant != .material3Expressive, 'Only material3 is supported.'); + : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// Overrides for [SubmenuButton] and [MenuItemButton]'s default style. /// diff --git a/packages/material_ui/lib/src/menu_theme.dart b/packages/material_ui/lib/src/menu_theme.dart index 5c0509b79f07..a933165f2f64 100644 --- a/packages/material_ui/lib/src/menu_theme.dart +++ b/packages/material_ui/lib/src/menu_theme.dart @@ -8,6 +8,7 @@ library; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'debug.dart'; import 'menu_anchor.dart'; import 'menu_style.dart'; import 'theme.dart'; @@ -38,7 +39,7 @@ import 'theme.dart'; class MenuThemeData with Diagnosticable { /// Creates a const set of properties used to configure [MenuTheme]. const MenuThemeData({this.style, this.submenuIcon, this.variant}) - : assert(variant != .material3Expressive, 'Only material3 is supported.'); + : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// The [MenuStyle] of a [SubmenuButton] menu. /// diff --git a/packages/material_ui/lib/src/navigation_bar.dart b/packages/material_ui/lib/src/navigation_bar.dart index 9e042a1a28dd..7170d28d387b 100644 --- a/packages/material_ui/lib/src/navigation_bar.dart +++ b/packages/material_ui/lib/src/navigation_bar.dart @@ -15,6 +15,7 @@ import 'package:flutter/widgets.dart'; import 'color_scheme.dart'; import 'colors.dart'; +import 'debug.dart'; import 'elevation_overlay.dart'; import 'ink_decoration.dart'; import 'ink_well.dart'; @@ -280,7 +281,7 @@ class NavigationBar extends StatelessWidget { final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context); final StyleVariant effectiveVariant = navigationBarTheme.variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final double effectiveHeight = height ?? navigationBarTheme.height ?? defaults.height!; final NavigationDestinationLabelBehavior effectiveLabelBehavior = labelBehavior ?? navigationBarTheme.labelBehavior ?? defaults.labelBehavior!; diff --git a/packages/material_ui/lib/src/navigation_bar_theme.dart b/packages/material_ui/lib/src/navigation_bar_theme.dart index c4608147ff9f..fc27b9589b2a 100644 --- a/packages/material_ui/lib/src/navigation_bar_theme.dart +++ b/packages/material_ui/lib/src/navigation_bar_theme.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'debug.dart'; import 'navigation_bar.dart'; import 'theme.dart'; @@ -53,7 +54,7 @@ class NavigationBarThemeData with Diagnosticable { this.overlayColor, this.labelPadding, this.variant, - }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); + }) : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// Overrides the default value of [NavigationBar.height]. final double? height; diff --git a/packages/material_ui/lib/src/navigation_rail.dart b/packages/material_ui/lib/src/navigation_rail.dart index e09739e221c0..518af95037b4 100644 --- a/packages/material_ui/lib/src/navigation_rail.dart +++ b/packages/material_ui/lib/src/navigation_rail.dart @@ -13,6 +13,7 @@ import 'dart:ui'; import 'package:flutter/widgets.dart'; import 'color_scheme.dart'; +import 'debug.dart'; import 'ink_well.dart'; import 'material.dart'; import 'material_localizations.dart'; @@ -447,7 +448,7 @@ class _NavigationRailState extends State with TickerProviderStat final ThemeData theme = Theme.of(context); final NavigationRailThemeData navigationRailTheme = NavigationRailTheme.of(context); final StyleVariant effectiveVariant = navigationRailTheme.variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final NavigationRailThemeData defaults = theme.useMaterial3 ? _NavigationRailDefaultsM3(context) : _NavigationRailDefaultsM2(context); diff --git a/packages/material_ui/lib/src/navigation_rail_theme.dart b/packages/material_ui/lib/src/navigation_rail_theme.dart index 36d6f6a9cbdc..e215fddc66f1 100644 --- a/packages/material_ui/lib/src/navigation_rail_theme.dart +++ b/packages/material_ui/lib/src/navigation_rail_theme.dart @@ -11,6 +11,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'debug.dart'; import 'navigation_rail.dart'; import 'theme.dart'; @@ -55,7 +56,7 @@ class NavigationRailThemeData with Diagnosticable { this.minWidth, this.minExtendedWidth, this.variant, - }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); + }) : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// Color to be used for the [NavigationRail]'s background. final Color? backgroundColor; diff --git a/packages/material_ui/lib/src/outlined_button.dart b/packages/material_ui/lib/src/outlined_button.dart index 456f45c2f86a..cd3697aa1200 100644 --- a/packages/material_ui/lib/src/outlined_button.dart +++ b/packages/material_ui/lib/src/outlined_button.dart @@ -18,6 +18,7 @@ import 'button_style_button.dart'; import 'color_scheme.dart'; import 'colors.dart'; import 'constants.dart'; +import 'debug.dart'; import 'ink_ripple.dart'; import 'ink_well.dart'; import 'material_state.dart'; @@ -349,7 +350,7 @@ class OutlinedButton extends ButtonStyleButton { ButtonStyle defaultStyleOf(BuildContext context) { final ThemeData theme = Theme.of(context); final StyleVariant effectiveVariant = OutlinedButtonTheme.of(context).variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final ColorScheme colorScheme = theme.colorScheme; final ButtonStyle buttonStyle = theme.useMaterial3 ? _OutlinedButtonDefaultsM3(context) diff --git a/packages/material_ui/lib/src/outlined_button_theme.dart b/packages/material_ui/lib/src/outlined_button_theme.dart index d84523b61d9b..e0a87cfe31ec 100644 --- a/packages/material_ui/lib/src/outlined_button_theme.dart +++ b/packages/material_ui/lib/src/outlined_button_theme.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'button_style.dart'; +import 'debug.dart'; import 'theme.dart'; // Examples can assume: @@ -40,7 +41,7 @@ class OutlinedButtonThemeData with Diagnosticable { /// /// The [style] may be null. const OutlinedButtonThemeData({this.style, this.variant}) - : assert(variant != .material3Expressive, 'Only material3 is supported.'); + : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// Overrides for [OutlinedButton]'s default style. /// diff --git a/packages/material_ui/lib/src/progress_indicator.dart b/packages/material_ui/lib/src/progress_indicator.dart index 911b83b69f9e..f072f9a40f74 100644 --- a/packages/material_ui/lib/src/progress_indicator.dart +++ b/packages/material_ui/lib/src/progress_indicator.dart @@ -14,6 +14,7 @@ import 'package:cupertino_ui/cupertino_ui.dart'; import 'package:flutter/foundation.dart'; import 'color_scheme.dart'; +import 'debug.dart'; import 'material.dart'; import 'progress_indicator_theme.dart'; import 'theme.dart'; @@ -650,7 +651,7 @@ class _LinearProgressIndicatorState extends State final ThemeData theme = Theme.of(context); final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context); final StyleVariant effectiveVariant = indicatorTheme.variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final TextDirection textDirection = Directionality.of(context); if (widget._effectiveValue != null) { @@ -1133,7 +1134,7 @@ class _CircularProgressIndicatorState extends State final ThemeData theme = Theme.of(context); final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context); final StyleVariant effectiveVariant = indicatorTheme.variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final bool year2023 = widget.year2023 ?? indicatorTheme.year2023 ?? true; final ProgressIndicatorThemeData defaults = switch (theme.useMaterial3) { true => diff --git a/packages/material_ui/lib/src/progress_indicator_theme.dart b/packages/material_ui/lib/src/progress_indicator_theme.dart index 64651caf836f..307e419208d4 100644 --- a/packages/material_ui/lib/src/progress_indicator_theme.dart +++ b/packages/material_ui/lib/src/progress_indicator_theme.dart @@ -11,6 +11,7 @@ import 'dart:ui' show lerpDouble; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'debug.dart'; import 'theme.dart'; // Examples can assume: @@ -58,7 +59,7 @@ class ProgressIndicatorThemeData with Diagnosticable { this.year2023, this.controller, this.variant, - }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); + }) : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// The color of the [ProgressIndicator]'s indicator. /// diff --git a/packages/material_ui/lib/src/range_slider.dart b/packages/material_ui/lib/src/range_slider.dart index ed3e549e6c1d..bd50df6c0c7d 100644 --- a/packages/material_ui/lib/src/range_slider.dart +++ b/packages/material_ui/lib/src/range_slider.dart @@ -654,7 +654,7 @@ class _RangeSliderState extends State with TickerProviderStateMixin final ThemeData theme = Theme.of(context); SliderThemeData sliderTheme = SliderTheme.of(context); final StyleVariant effectiveVariant = sliderTheme.variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final bool year2023 = widget.year2023 ?? sliderTheme.year2023 ?? true; final SliderThemeData defaults = theme.useMaterial3 && !year2023 ? _RangeSliderDefaultsM3(context) diff --git a/packages/material_ui/lib/src/search_anchor.dart b/packages/material_ui/lib/src/search_anchor.dart index daa734b35a5c..da98e510a370 100644 --- a/packages/material_ui/lib/src/search_anchor.dart +++ b/packages/material_ui/lib/src/search_anchor.dart @@ -19,6 +19,7 @@ import 'button_style.dart'; import 'color_scheme.dart'; import 'colors.dart'; import 'constants.dart'; +import 'debug.dart'; import 'divider.dart'; import 'divider_theme.dart'; import 'icon_button.dart'; @@ -571,7 +572,7 @@ class _SearchAnchorState extends State { final ThemeData theme = Theme.of(context); final SearchViewThemeData viewTheme = SearchViewTheme.of(context); final StyleVariant effectiveVariant = viewTheme.variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); return AnimatedOpacity( key: _anchorKey, @@ -1670,7 +1671,7 @@ class _SearchBarState extends State { final ColorScheme colorScheme = theme.colorScheme; final SearchBarThemeData searchBarTheme = SearchBarTheme.of(context); final StyleVariant effectiveVariant = searchBarTheme.variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final SearchBarThemeData defaults = _SearchBarDefaultsM3(context); T? resolve( diff --git a/packages/material_ui/lib/src/search_bar_theme.dart b/packages/material_ui/lib/src/search_bar_theme.dart index eb470a1ef8a4..21668d5e9a96 100644 --- a/packages/material_ui/lib/src/search_bar_theme.dart +++ b/packages/material_ui/lib/src/search_bar_theme.dart @@ -12,6 +12,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'debug.dart'; import 'theme.dart'; // Examples can assume: @@ -52,7 +53,7 @@ class SearchBarThemeData with Diagnosticable { this.constraints, this.textCapitalization, this.variant, - }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); + }) : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// Overrides the default value of the [SearchBar.elevation]. final WidgetStateProperty? elevation; diff --git a/packages/material_ui/lib/src/search_view_theme.dart b/packages/material_ui/lib/src/search_view_theme.dart index 1ca4a7d39b11..4b729e9107c6 100644 --- a/packages/material_ui/lib/src/search_view_theme.dart +++ b/packages/material_ui/lib/src/search_view_theme.dart @@ -12,6 +12,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'debug.dart'; import 'theme.dart'; // Examples can assume: @@ -54,7 +55,7 @@ class SearchViewThemeData with Diagnosticable { this.headerHintStyle, this.dividerColor, this.variant, - }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); + }) : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// Overrides the default value of the [SearchAnchor.viewBackgroundColor]. final Color? backgroundColor; @@ -94,7 +95,7 @@ class SearchViewThemeData with Diagnosticable { /// Overrides the value of the divider color for [SearchAnchor.dividerColor]. final Color? dividerColor; - + /// The style variant of Material Design used by search views. final StyleVariant? variant; diff --git a/packages/material_ui/lib/src/slider.dart b/packages/material_ui/lib/src/slider.dart index 26ad239b388e..ac843b91ed51 100644 --- a/packages/material_ui/lib/src/slider.dart +++ b/packages/material_ui/lib/src/slider.dart @@ -832,7 +832,7 @@ class _SliderState extends State with TickerProviderStateMixin { final ThemeData theme = Theme.of(context); SliderThemeData sliderTheme = SliderTheme.of(context); final StyleVariant effectiveVariant = sliderTheme.variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final bool year2023 = widget.year2023 ?? sliderTheme.year2023 ?? true; final SliderThemeData defaults = switch (theme.useMaterial3) { true => year2023 ? _SliderDefaultsM3Year2023(context) : _SliderDefaultsM3(context), diff --git a/packages/material_ui/lib/src/slider_theme.dart b/packages/material_ui/lib/src/slider_theme.dart index b1888cb0c845..ff3e7f98b68f 100644 --- a/packages/material_ui/lib/src/slider_theme.dart +++ b/packages/material_ui/lib/src/slider_theme.dart @@ -14,6 +14,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; +import 'debug.dart'; import 'range_slider_parts.dart'; import 'slider.dart'; import 'slider_parts.dart'; @@ -319,7 +320,7 @@ class SliderThemeData with Diagnosticable { ) this.year2023, this.variant, - }) : assert(variant != .material3Expressive, 'Only material3 is supported.'); + }) : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// Generates a SliderThemeData from three main colors. /// @@ -659,7 +660,7 @@ class SliderThemeData with Diagnosticable { 'This feature was deprecated after v3.27.0-0.2.pre.', ) final bool? year2023; - + /// The style variant of Material Design used by sliders. final StyleVariant? variant; diff --git a/packages/material_ui/lib/src/text_button.dart b/packages/material_ui/lib/src/text_button.dart index 2119c715798d..e7e28ef9a140 100644 --- a/packages/material_ui/lib/src/text_button.dart +++ b/packages/material_ui/lib/src/text_button.dart @@ -18,6 +18,7 @@ import 'button_style_button.dart'; import 'color_scheme.dart'; import 'colors.dart'; import 'constants.dart'; +import 'debug.dart'; import 'ink_ripple.dart'; import 'ink_well.dart'; import 'material_state.dart'; @@ -379,7 +380,7 @@ class TextButton extends ButtonStyleButton { ButtonStyle defaultStyleOf(BuildContext context) { final ThemeData theme = Theme.of(context); final StyleVariant effectiveVariant = TextButtonTheme.of(context).variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, 'Only material3 is supported.'); + assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final ColorScheme colorScheme = theme.colorScheme; final ButtonStyle buttonStyle = theme.useMaterial3 ? _TextButtonDefaultsM3(context) diff --git a/packages/material_ui/lib/src/text_button_theme.dart b/packages/material_ui/lib/src/text_button_theme.dart index ac25a028f6fa..1e4e52d035ea 100644 --- a/packages/material_ui/lib/src/text_button_theme.dart +++ b/packages/material_ui/lib/src/text_button_theme.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'button_style.dart'; +import 'debug.dart'; import 'theme.dart'; // Examples can assume: @@ -40,7 +41,7 @@ class TextButtonThemeData with Diagnosticable { /// /// The [style] may be null. const TextButtonThemeData({this.style, this.variant}) - : assert(variant != .material3Expressive, 'Only material3 is supported.'); + : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); /// Overrides for [TextButton]'s default style. /// diff --git a/packages/material_ui/lib/src/theme_data.dart b/packages/material_ui/lib/src/theme_data.dart index 369eb84bdb8f..43a36250b45d 100644 --- a/packages/material_ui/lib/src/theme_data.dart +++ b/packages/material_ui/lib/src/theme_data.dart @@ -29,6 +29,7 @@ import 'colors.dart'; import 'constants.dart'; import 'data_table_theme.dart'; import 'date_picker_theme.dart'; +import 'debug.dart'; import 'dialog_theme.dart'; import 'divider_theme.dart'; import 'drawer_theme.dart'; @@ -396,7 +397,7 @@ class ThemeData with Diagnosticable { extensions ??= >[]; adaptations ??= >[]; variant ??= StyleVariant.material3; - assert(variant != .material3Expressive, 'Only material3 is supported.'); + assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); // TODO(bleroux): Clean this up once the type of `inputDecorationTheme` is changed to `InputDecorationThemeData` if (inputDecorationTheme != null) { if (inputDecorationTheme is InputDecorationTheme) { @@ -820,7 +821,7 @@ class ThemeData with Diagnosticable { 'This feature was deprecated after v3.28.0-1.0.pre.', ) required this.indicatorColor, - }) : assert(variant != .material3Expressive, 'Only material3 is supported.'), + }) : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage), // DEPRECATED (newest deprecations at the bottom) // should not be `required`, use getter pattern to avoid breakages. _buttonBarTheme = buttonBarTheme, From cea10da425e741be6b023a652696318e08083755 Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Tue, 23 Jun 2026 13:58:53 -0700 Subject: [PATCH 5/7] Address feedback --- packages/material_ui/lib/src/debug.dart | 3 ++- packages/material_ui/test/app_bar_theme_test.dart | 2 +- packages/material_ui/test/elevated_button_theme_test.dart | 2 +- packages/material_ui/test/filled_button_theme_test.dart | 2 +- .../material_ui/test/floating_action_button_theme_test.dart | 2 +- packages/material_ui/test/icon_button_theme_test.dart | 2 +- packages/material_ui/test/menu_button_theme_test.dart | 2 +- packages/material_ui/test/menu_theme_test.dart | 2 +- packages/material_ui/test/navigation_bar_theme_test.dart | 2 +- packages/material_ui/test/navigation_rail_theme_test.dart | 2 +- packages/material_ui/test/outlined_button_theme_test.dart | 2 +- .../material_ui/test/progress_indicator_theme_test.dart | 2 +- packages/material_ui/test/search_bar_theme_test.dart | 2 +- packages/material_ui/test/search_view_theme_test.dart | 2 +- packages/material_ui/test/slider_theme_test.dart | 2 +- packages/material_ui/test/text_button_theme_test.dart | 2 +- packages/material_ui/test/theme_data_test.dart | 6 +++--- 17 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/material_ui/lib/src/debug.dart b/packages/material_ui/lib/src/debug.dart index 6e29e543273d..ab74005ebb4e 100644 --- a/packages/material_ui/lib/src/debug.dart +++ b/packages/material_ui/lib/src/debug.dart @@ -13,7 +13,8 @@ import 'scaffold.dart' show Scaffold, ScaffoldMessenger; /// The assertion message used while Material components do not yet support /// Material 3 Expressive. -const String kUnsupportedStyleVariantAssertionMessage = 'Only material3 is supported.'; +const String kUnsupportedStyleVariantAssertionMessage = + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.'; /// Asserts that the given context has a [Material] ancestor within the closest /// [LookupBoundary]. diff --git a/packages/material_ui/test/app_bar_theme_test.dart b/packages/material_ui/test/app_bar_theme_test.dart index 4d27d489f728..12f5fe262790 100644 --- a/packages/material_ui/test/app_bar_theme_test.dart +++ b/packages/material_ui/test/app_bar_theme_test.dart @@ -19,7 +19,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/elevated_button_theme_test.dart b/packages/material_ui/test/elevated_button_theme_test.dart index b5a69e1c02c5..b0fcec5a35ae 100644 --- a/packages/material_ui/test/elevated_button_theme_test.dart +++ b/packages/material_ui/test/elevated_button_theme_test.dart @@ -16,7 +16,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/filled_button_theme_test.dart b/packages/material_ui/test/filled_button_theme_test.dart index 0356a0d7f030..25b5166a821d 100644 --- a/packages/material_ui/test/filled_button_theme_test.dart +++ b/packages/material_ui/test/filled_button_theme_test.dart @@ -16,7 +16,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/floating_action_button_theme_test.dart b/packages/material_ui/test/floating_action_button_theme_test.dart index a69bc40b4b61..d42ba7cf154a 100644 --- a/packages/material_ui/test/floating_action_button_theme_test.dart +++ b/packages/material_ui/test/floating_action_button_theme_test.dart @@ -18,7 +18,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/icon_button_theme_test.dart b/packages/material_ui/test/icon_button_theme_test.dart index c6265d18749b..38bd957317fe 100644 --- a/packages/material_ui/test/icon_button_theme_test.dart +++ b/packages/material_ui/test/icon_button_theme_test.dart @@ -44,7 +44,7 @@ void main() { isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ), ); }); diff --git a/packages/material_ui/test/menu_button_theme_test.dart b/packages/material_ui/test/menu_button_theme_test.dart index dc050ca89db9..ab4e1a747c21 100644 --- a/packages/material_ui/test/menu_button_theme_test.dart +++ b/packages/material_ui/test/menu_button_theme_test.dart @@ -16,7 +16,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/menu_theme_test.dart b/packages/material_ui/test/menu_theme_test.dart index e04baec96fd8..fc2ce6b51f5b 100644 --- a/packages/material_ui/test/menu_theme_test.dart +++ b/packages/material_ui/test/menu_theme_test.dart @@ -18,7 +18,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/navigation_bar_theme_test.dart b/packages/material_ui/test/navigation_bar_theme_test.dart index 503bc2ba0b00..712b5aea604e 100644 --- a/packages/material_ui/test/navigation_bar_theme_test.dart +++ b/packages/material_ui/test/navigation_bar_theme_test.dart @@ -25,7 +25,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/navigation_rail_theme_test.dart b/packages/material_ui/test/navigation_rail_theme_test.dart index ef119e1ef173..990753f6a2f5 100644 --- a/packages/material_ui/test/navigation_rail_theme_test.dart +++ b/packages/material_ui/test/navigation_rail_theme_test.dart @@ -17,7 +17,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/outlined_button_theme_test.dart b/packages/material_ui/test/outlined_button_theme_test.dart index a096ee7791cc..7ea28902b4ae 100644 --- a/packages/material_ui/test/outlined_button_theme_test.dart +++ b/packages/material_ui/test/outlined_button_theme_test.dart @@ -16,7 +16,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/progress_indicator_theme_test.dart b/packages/material_ui/test/progress_indicator_theme_test.dart index 319831b1a686..ad7dcb541040 100644 --- a/packages/material_ui/test/progress_indicator_theme_test.dart +++ b/packages/material_ui/test/progress_indicator_theme_test.dart @@ -23,7 +23,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/search_bar_theme_test.dart b/packages/material_ui/test/search_bar_theme_test.dart index 5b94cd71fedb..0db0b7d02834 100644 --- a/packages/material_ui/test/search_bar_theme_test.dart +++ b/packages/material_ui/test/search_bar_theme_test.dart @@ -17,7 +17,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/search_view_theme_test.dart b/packages/material_ui/test/search_view_theme_test.dart index ded4fbf074e7..e302915dec0b 100644 --- a/packages/material_ui/test/search_view_theme_test.dart +++ b/packages/material_ui/test/search_view_theme_test.dart @@ -17,7 +17,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/slider_theme_test.dart b/packages/material_ui/test/slider_theme_test.dart index 2089409a8d55..45c1b3c35c8e 100644 --- a/packages/material_ui/test/slider_theme_test.dart +++ b/packages/material_ui/test/slider_theme_test.dart @@ -18,7 +18,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/text_button_theme_test.dart b/packages/material_ui/test/text_button_theme_test.dart index 94829cbdea65..3ce995ffa01e 100644 --- a/packages/material_ui/test/text_button_theme_test.dart +++ b/packages/material_ui/test/text_button_theme_test.dart @@ -16,7 +16,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ); } diff --git a/packages/material_ui/test/theme_data_test.dart b/packages/material_ui/test/theme_data_test.dart index f046d3627999..b51cf7b7bf8a 100644 --- a/packages/material_ui/test/theme_data_test.dart +++ b/packages/material_ui/test/theme_data_test.dart @@ -37,7 +37,7 @@ void main() { isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ), ); } @@ -73,7 +73,7 @@ void main() { isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ), ), ); @@ -85,7 +85,7 @@ void main() { isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported.', + 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', ), ); } From 3084bc42683a1fcc7bc634ba99c7a918f99d9b20 Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Tue, 23 Jun 2026 14:39:22 -0700 Subject: [PATCH 6/7] Update tests --- packages/material_ui/test/app_bar_theme_test.dart | 2 +- packages/material_ui/test/elevated_button_theme_test.dart | 2 +- packages/material_ui/test/filled_button_theme_test.dart | 2 +- .../material_ui/test/floating_action_button_theme_test.dart | 2 +- packages/material_ui/test/icon_button_theme_test.dart | 2 +- packages/material_ui/test/menu_button_theme_test.dart | 2 +- packages/material_ui/test/menu_theme_test.dart | 2 +- packages/material_ui/test/navigation_bar_theme_test.dart | 2 +- packages/material_ui/test/navigation_rail_theme_test.dart | 2 +- packages/material_ui/test/outlined_button_theme_test.dart | 2 +- .../material_ui/test/progress_indicator_theme_test.dart | 2 +- packages/material_ui/test/search_bar_theme_test.dart | 2 +- packages/material_ui/test/search_view_theme_test.dart | 2 +- packages/material_ui/test/slider_theme_test.dart | 2 +- packages/material_ui/test/text_button_theme_test.dart | 2 +- packages/material_ui/test/theme_data_test.dart | 6 +++--- 16 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/material_ui/test/app_bar_theme_test.dart b/packages/material_ui/test/app_bar_theme_test.dart index 12f5fe262790..b078bf37c0d1 100644 --- a/packages/material_ui/test/app_bar_theme_test.dart +++ b/packages/material_ui/test/app_bar_theme_test.dart @@ -19,7 +19,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/elevated_button_theme_test.dart b/packages/material_ui/test/elevated_button_theme_test.dart index b0fcec5a35ae..643d40286cc1 100644 --- a/packages/material_ui/test/elevated_button_theme_test.dart +++ b/packages/material_ui/test/elevated_button_theme_test.dart @@ -16,7 +16,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/filled_button_theme_test.dart b/packages/material_ui/test/filled_button_theme_test.dart index 25b5166a821d..9e2048147936 100644 --- a/packages/material_ui/test/filled_button_theme_test.dart +++ b/packages/material_ui/test/filled_button_theme_test.dart @@ -16,7 +16,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/floating_action_button_theme_test.dart b/packages/material_ui/test/floating_action_button_theme_test.dart index d42ba7cf154a..bc40c318811e 100644 --- a/packages/material_ui/test/floating_action_button_theme_test.dart +++ b/packages/material_ui/test/floating_action_button_theme_test.dart @@ -18,7 +18,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/icon_button_theme_test.dart b/packages/material_ui/test/icon_button_theme_test.dart index 38bd957317fe..6ad4e03ad603 100644 --- a/packages/material_ui/test/icon_button_theme_test.dart +++ b/packages/material_ui/test/icon_button_theme_test.dart @@ -44,7 +44,7 @@ void main() { isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ), ); }); diff --git a/packages/material_ui/test/menu_button_theme_test.dart b/packages/material_ui/test/menu_button_theme_test.dart index ab4e1a747c21..fbf64723c5ba 100644 --- a/packages/material_ui/test/menu_button_theme_test.dart +++ b/packages/material_ui/test/menu_button_theme_test.dart @@ -16,7 +16,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/menu_theme_test.dart b/packages/material_ui/test/menu_theme_test.dart index fc2ce6b51f5b..59512bb6b4a7 100644 --- a/packages/material_ui/test/menu_theme_test.dart +++ b/packages/material_ui/test/menu_theme_test.dart @@ -18,7 +18,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/navigation_bar_theme_test.dart b/packages/material_ui/test/navigation_bar_theme_test.dart index 712b5aea604e..ef17b32445a0 100644 --- a/packages/material_ui/test/navigation_bar_theme_test.dart +++ b/packages/material_ui/test/navigation_bar_theme_test.dart @@ -25,7 +25,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/navigation_rail_theme_test.dart b/packages/material_ui/test/navigation_rail_theme_test.dart index 990753f6a2f5..01549a6ec638 100644 --- a/packages/material_ui/test/navigation_rail_theme_test.dart +++ b/packages/material_ui/test/navigation_rail_theme_test.dart @@ -17,7 +17,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/outlined_button_theme_test.dart b/packages/material_ui/test/outlined_button_theme_test.dart index 7ea28902b4ae..5e23b6f444af 100644 --- a/packages/material_ui/test/outlined_button_theme_test.dart +++ b/packages/material_ui/test/outlined_button_theme_test.dart @@ -16,7 +16,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/progress_indicator_theme_test.dart b/packages/material_ui/test/progress_indicator_theme_test.dart index ad7dcb541040..c96e05c49dc6 100644 --- a/packages/material_ui/test/progress_indicator_theme_test.dart +++ b/packages/material_ui/test/progress_indicator_theme_test.dart @@ -23,7 +23,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/search_bar_theme_test.dart b/packages/material_ui/test/search_bar_theme_test.dart index 0db0b7d02834..c518f5e37cea 100644 --- a/packages/material_ui/test/search_bar_theme_test.dart +++ b/packages/material_ui/test/search_bar_theme_test.dart @@ -17,7 +17,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/search_view_theme_test.dart b/packages/material_ui/test/search_view_theme_test.dart index e302915dec0b..fe0d8119ba3f 100644 --- a/packages/material_ui/test/search_view_theme_test.dart +++ b/packages/material_ui/test/search_view_theme_test.dart @@ -17,7 +17,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/slider_theme_test.dart b/packages/material_ui/test/slider_theme_test.dart index 45c1b3c35c8e..12d2000a48ec 100644 --- a/packages/material_ui/test/slider_theme_test.dart +++ b/packages/material_ui/test/slider_theme_test.dart @@ -18,7 +18,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/text_button_theme_test.dart b/packages/material_ui/test/text_button_theme_test.dart index 3ce995ffa01e..27b4827a4268 100644 --- a/packages/material_ui/test/text_button_theme_test.dart +++ b/packages/material_ui/test/text_button_theme_test.dart @@ -16,7 +16,7 @@ Matcher get _throwsUnsupportedStyleVariantAssertion { return isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ); } diff --git a/packages/material_ui/test/theme_data_test.dart b/packages/material_ui/test/theme_data_test.dart index b51cf7b7bf8a..4f1517b3e02a 100644 --- a/packages/material_ui/test/theme_data_test.dart +++ b/packages/material_ui/test/theme_data_test.dart @@ -37,7 +37,7 @@ void main() { isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ), ); } @@ -73,7 +73,7 @@ void main() { isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ), ), ); @@ -85,7 +85,7 @@ void main() { isA().having( (AssertionError error) => error.message, 'message', - 'Only material3 is supported. See https://github.com/orgs/flutter/projects/250 to track support for material3Expressive.', + kUnsupportedStyleVariantAssertionMessage, ), ); } From 5130683688ff289b5d6078e416f4760f1b532003 Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Thu, 28 May 2026 17:24:05 -0700 Subject: [PATCH 7/7] Add M3E IconButton --- .../material_ui/lib/src/button_style.dart | 58 +- .../generated/icon_button_m3e_defaults.g.dart | 924 ++++++++++++++++++ packages/material_ui/lib/src/icon_button.dart | 72 +- .../lib/src/icon_button_theme.dart | 4 +- .../test/icon_button_theme_test.dart | 33 +- ...aterial_3_expressive_icon_button_test.dart | 655 +++++++++++++ .../material_ui/test/theme_data_test.dart | 1 - .../tool/gen_defaults/bin/gen_defaults.dart | 5 +- .../templates/icon_button_template.dart | 697 +++++++++++++ .../tool/gen_defaults/templates/template.dart | 48 + 10 files changed, 2462 insertions(+), 35 deletions(-) create mode 100644 packages/material_ui/lib/src/generated/icon_button_m3e_defaults.g.dart create mode 100644 packages/material_ui/test/material_3_expressive_icon_button_test.dart create mode 100644 packages/material_ui/tool/gen_defaults/templates/icon_button_template.dart diff --git a/packages/material_ui/lib/src/button_style.dart b/packages/material_ui/lib/src/button_style.dart index e251296f42ed..8409323a94e3 100644 --- a/packages/material_ui/lib/src/button_style.dart +++ b/packages/material_ui/lib/src/button_style.dart @@ -31,6 +31,38 @@ import 'theme_data.dart'; // late BuildContext context; // typedef MyAppHome = Placeholder; +/// Defines size variants for Material 3 Expressive button components. +/// +/// Components interpret each size variant according to their own token set. +enum ButtonSize { + /// Extra small button size. + xSmall, + + /// Small button size. This is the default for icon buttons. + small, + + /// Medium button size. + medium, + + /// Large button size. + large, + + /// Extra large button size. + xLarge, +} + +/// Defines the width variants for Material 3 Expressive [IconButton]. +enum IconButtonWidth { + /// Uses the narrow leading and trailing space tokens. + narrow, + + /// Uses the default leading and trailing space tokens. + standard, + + /// Uses the wide leading and trailing space tokens. + wide, +} + /// The type for [ButtonStyle.backgroundBuilder] and [ButtonStyle.foregroundBuilder]. /// /// The [states] parameter is the button's current pressed/hovered/etc state. The [child] is @@ -187,6 +219,8 @@ class ButtonStyle with Diagnosticable { this.splashFactory, this.backgroundBuilder, this.foregroundBuilder, + this.size, + this.iconButtonWidth, }); /// The style for a button's [Text] widget descendants. @@ -423,6 +457,12 @@ class ButtonStyle with Diagnosticable { /// configuring clipping. final ButtonLayerBuilder? foregroundBuilder; + /// The size variant for this button. + final ButtonSize? size; + + /// The width variant for this icon button. + final IconButtonWidth? iconButtonWidth; + /// Returns a copy of this ButtonStyle with the given fields replaced with /// the new values. ButtonStyle copyWith({ @@ -451,6 +491,8 @@ class ButtonStyle with Diagnosticable { InteractiveInkFeatureFactory? splashFactory, ButtonLayerBuilder? backgroundBuilder, ButtonLayerBuilder? foregroundBuilder, + ButtonSize? size, + IconButtonWidth? iconButtonWidth, }) { return ButtonStyle( textStyle: textStyle ?? this.textStyle, @@ -478,6 +520,8 @@ class ButtonStyle with Diagnosticable { splashFactory: splashFactory ?? this.splashFactory, backgroundBuilder: backgroundBuilder ?? this.backgroundBuilder, foregroundBuilder: foregroundBuilder ?? this.foregroundBuilder, + size: size ?? this.size, + iconButtonWidth: iconButtonWidth ?? this.iconButtonWidth, ); } @@ -516,6 +560,8 @@ class ButtonStyle with Diagnosticable { splashFactory: splashFactory ?? style.splashFactory, backgroundBuilder: backgroundBuilder ?? style.backgroundBuilder, foregroundBuilder: foregroundBuilder ?? style.foregroundBuilder, + size: size ?? style.size, + iconButtonWidth: iconButtonWidth ?? style.iconButtonWidth, ); } @@ -547,6 +593,8 @@ class ButtonStyle with Diagnosticable { splashFactory, backgroundBuilder, foregroundBuilder, + size, + iconButtonWidth, ]; return Object.hashAll(values); } @@ -584,7 +632,9 @@ class ButtonStyle with Diagnosticable { other.alignment == alignment && other.splashFactory == splashFactory && other.backgroundBuilder == backgroundBuilder && - other.foregroundBuilder == foregroundBuilder; + other.foregroundBuilder == foregroundBuilder && + other.size == size && + other.iconButtonWidth == iconButtonWidth; } @override @@ -706,6 +756,10 @@ class ButtonStyle with Diagnosticable { defaultValue: null, ), ); + properties.add(EnumProperty('size', size, defaultValue: null)); + properties.add( + EnumProperty('iconButtonWidth', iconButtonWidth, defaultValue: null), + ); } /// Linearly interpolate between two [ButtonStyle]s. @@ -769,6 +823,8 @@ class ButtonStyle with Diagnosticable { splashFactory: t < 0.5 ? a?.splashFactory : b?.splashFactory, backgroundBuilder: t < 0.5 ? a?.backgroundBuilder : b?.backgroundBuilder, foregroundBuilder: t < 0.5 ? a?.foregroundBuilder : b?.foregroundBuilder, + size: t < 0.5 ? a?.size : b?.size, + iconButtonWidth: t < 0.5 ? a?.iconButtonWidth : b?.iconButtonWidth, ); } } diff --git a/packages/material_ui/lib/src/generated/icon_button_m3e_defaults.g.dart b/packages/material_ui/lib/src/generated/icon_button_m3e_defaults.g.dart new file mode 100644 index 000000000000..b13085c26402 --- /dev/null +++ b/packages/material_ui/lib/src/generated/icon_button_m3e_defaults.g.dart @@ -0,0 +1,924 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Do not edit by hand. The code is generated from data in the Material +// Design token database by the script: +// packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart. +part of '../icon_button.dart'; + +class _M3EIconButtonDefaults extends ButtonStyle { + _M3EIconButtonDefaults(this.context, this.toggleable, this.buttonSize, this.buttonWidth) + : super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; + final ButtonSize? buttonSize; + final IconButtonWidth? buttonWidth; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + WidgetStateProperty? get backgroundColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get foregroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + if (toggleable && states.contains(WidgetState.selected)) { + return _colors.primary; + } + return _colors.onSurfaceVariant; + }); + + @override + WidgetStateProperty? get overlayColor => + WidgetStateProperty.resolveWith((Set states) { + if (toggleable && states.contains(WidgetState.selected)) { + if (states.contains(WidgetState.pressed)) { + return _colors.primary.withOpacity(0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.primary.withOpacity(0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.primary.withOpacity(0.1); + } + } + if (states.contains(WidgetState.pressed)) { + return _colors.onSurfaceVariant.withOpacity(0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.onSurfaceVariant.withOpacity(0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.onSurfaceVariant.withOpacity(0.1); + } + return Colors.transparent; + }); + + @override + WidgetStateProperty? get elevation => const MaterialStatePropertyAll(0.0); + + @override + WidgetStateProperty? get shadowColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get surfaceTintColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get padding => + MaterialStatePropertyAll(switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 6.0, 4.0, 6.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(6.0, 6.0, 6.0, 6.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(10.0, 6.0, 10.0, 6.0), + }, + ButtonSize.small => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 8.0, 4.0, 8.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(8.0, 8.0, 8.0, 8.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(14.0, 8.0, 14.0, 8.0), + }, + ButtonSize.medium => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(12.0, 16.0, 12.0, 16.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(16.0, 16.0, 16.0, 16.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(24.0, 16.0, 24.0, 16.0), + }, + ButtonSize.large => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(16.0, 32.0, 16.0, 32.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(32.0, 32.0, 32.0, 32.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(48.0, 32.0, 48.0, 32.0), + }, + ButtonSize.xLarge => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(32.0, 48.0, 32.0, 48.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(48.0, 48.0, 48.0, 48.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(72.0, 48.0, 72.0, 48.0), + }, + }); + + @override + WidgetStateProperty? get minimumSize => + MaterialStatePropertyAll(switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(28.0, 32.0), + IconButtonWidth.standard => const Size(32.0, 32.0), + IconButtonWidth.wide => const Size(40.0, 32.0), + }, + ButtonSize.small => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(32.0, 40.0), + IconButtonWidth.standard => const Size(40.0, 40.0), + IconButtonWidth.wide => const Size(52.0, 40.0), + }, + ButtonSize.medium => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(48.0, 56.0), + IconButtonWidth.standard => const Size(56.0, 56.0), + IconButtonWidth.wide => const Size(72.0, 56.0), + }, + ButtonSize.large => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(64.0, 96.0), + IconButtonWidth.standard => const Size(96.0, 96.0), + IconButtonWidth.wide => const Size(128.0, 96.0), + }, + ButtonSize.xLarge => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(104.0, 136.0), + IconButtonWidth.standard => const Size(136.0, 136.0), + IconButtonWidth.wide => const Size(184.0, 136.0), + }, + }); + + @override + WidgetStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); + + @override + WidgetStateProperty? get iconSize => + MaterialStatePropertyAll(switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => 20.0, + ButtonSize.small => 24.0, + ButtonSize.medium => 24.0, + ButtonSize.large => 32.0, + ButtonSize.xLarge => 40.0, + }); + + @override + WidgetStateProperty? get shape => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.pressed)) { + return switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSize.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSize.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSize.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSize.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + }; + } + if (toggleable && states.contains(WidgetState.selected)) { + return switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSize.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSize.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSize.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + ButtonSize.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + }; + } + return switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => const StadiumBorder(), + ButtonSize.small => const StadiumBorder(), + ButtonSize.medium => const StadiumBorder(), + ButtonSize.large => const StadiumBorder(), + ButtonSize.xLarge => const StadiumBorder(), + }; + }); + + @override + WidgetStateProperty? get side => null; + + @override + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; + + @override + VisualDensity? get visualDensity => VisualDensity.standard; + + @override + MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} + +class _M3EFilledIconButtonDefaults extends ButtonStyle { + _M3EFilledIconButtonDefaults(this.context, this.toggleable, this.buttonSize, this.buttonWidth) + : super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; + final ButtonSize? buttonSize; + final IconButtonWidth? buttonWidth; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + WidgetStateProperty? get backgroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return _colors.onSurface.withOpacity(0.1); + } + if (toggleable && states.contains(WidgetState.selected)) { + return _colors.primary; + } + if (toggleable) { + return _colors.surfaceContainer; + } + return _colors.primary; + }); + + @override + WidgetStateProperty? get foregroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + if (toggleable && states.contains(WidgetState.selected)) { + return _colors.onPrimary; + } + if (toggleable) { + return _colors.onSurfaceVariant; + } + return _colors.onPrimary; + }); + + @override + WidgetStateProperty? get overlayColor => + WidgetStateProperty.resolveWith((Set states) { + if (toggleable && states.contains(WidgetState.selected)) { + if (states.contains(WidgetState.pressed)) { + return _colors.onPrimary.withOpacity(0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.onPrimary.withOpacity(0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.onPrimary.withOpacity(0.1); + } + } + if (toggleable) { + if (states.contains(WidgetState.pressed)) { + return _colors.onSurfaceVariant.withOpacity(0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.onSurfaceVariant.withOpacity(0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.onSurfaceVariant.withOpacity(0.1); + } + } + if (states.contains(WidgetState.pressed)) { + return _colors.onPrimary.withOpacity(0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.onPrimary.withOpacity(0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.onPrimary.withOpacity(0.1); + } + return Colors.transparent; + }); + + @override + WidgetStateProperty? get elevation => const MaterialStatePropertyAll(0.0); + + @override + WidgetStateProperty? get shadowColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get surfaceTintColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get padding => + MaterialStatePropertyAll(switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 6.0, 4.0, 6.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(6.0, 6.0, 6.0, 6.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(10.0, 6.0, 10.0, 6.0), + }, + ButtonSize.small => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 8.0, 4.0, 8.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(8.0, 8.0, 8.0, 8.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(14.0, 8.0, 14.0, 8.0), + }, + ButtonSize.medium => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(12.0, 16.0, 12.0, 16.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(16.0, 16.0, 16.0, 16.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(24.0, 16.0, 24.0, 16.0), + }, + ButtonSize.large => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(16.0, 32.0, 16.0, 32.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(32.0, 32.0, 32.0, 32.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(48.0, 32.0, 48.0, 32.0), + }, + ButtonSize.xLarge => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(32.0, 48.0, 32.0, 48.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(48.0, 48.0, 48.0, 48.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(72.0, 48.0, 72.0, 48.0), + }, + }); + + @override + WidgetStateProperty? get minimumSize => + MaterialStatePropertyAll(switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(28.0, 32.0), + IconButtonWidth.standard => const Size(32.0, 32.0), + IconButtonWidth.wide => const Size(40.0, 32.0), + }, + ButtonSize.small => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(32.0, 40.0), + IconButtonWidth.standard => const Size(40.0, 40.0), + IconButtonWidth.wide => const Size(52.0, 40.0), + }, + ButtonSize.medium => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(48.0, 56.0), + IconButtonWidth.standard => const Size(56.0, 56.0), + IconButtonWidth.wide => const Size(72.0, 56.0), + }, + ButtonSize.large => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(64.0, 96.0), + IconButtonWidth.standard => const Size(96.0, 96.0), + IconButtonWidth.wide => const Size(128.0, 96.0), + }, + ButtonSize.xLarge => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(104.0, 136.0), + IconButtonWidth.standard => const Size(136.0, 136.0), + IconButtonWidth.wide => const Size(184.0, 136.0), + }, + }); + + @override + WidgetStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); + + @override + WidgetStateProperty? get iconSize => + MaterialStatePropertyAll(switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => 20.0, + ButtonSize.small => 24.0, + ButtonSize.medium => 24.0, + ButtonSize.large => 32.0, + ButtonSize.xLarge => 40.0, + }); + + @override + WidgetStateProperty? get shape => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.pressed)) { + return switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSize.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSize.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSize.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSize.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + }; + } + if (toggleable && states.contains(WidgetState.selected)) { + return switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSize.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSize.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSize.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + ButtonSize.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + }; + } + return switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => const StadiumBorder(), + ButtonSize.small => const StadiumBorder(), + ButtonSize.medium => const StadiumBorder(), + ButtonSize.large => const StadiumBorder(), + ButtonSize.xLarge => const StadiumBorder(), + }; + }); + + @override + WidgetStateProperty? get side => null; + + @override + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; + + @override + VisualDensity? get visualDensity => VisualDensity.standard; + + @override + MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} + +class _M3EFilledTonalIconButtonDefaults extends ButtonStyle { + _M3EFilledTonalIconButtonDefaults( + this.context, + this.toggleable, + this.buttonSize, + this.buttonWidth, + ) : super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; + final ButtonSize? buttonSize; + final IconButtonWidth? buttonWidth; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + WidgetStateProperty? get backgroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return _colors.onSurface.withOpacity(0.1); + } + if (toggleable && states.contains(WidgetState.selected)) { + return _colors.secondary; + } + if (toggleable) { + return _colors.secondaryContainer; + } + return _colors.secondaryContainer; + }); + + @override + WidgetStateProperty? get foregroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + if (toggleable && states.contains(WidgetState.selected)) { + return _colors.onSecondary; + } + if (toggleable) { + return _colors.onSecondaryContainer; + } + return _colors.onSecondaryContainer; + }); + + @override + WidgetStateProperty? get overlayColor => + WidgetStateProperty.resolveWith((Set states) { + if (toggleable && states.contains(WidgetState.selected)) { + if (states.contains(WidgetState.pressed)) { + return _colors.onSecondary.withOpacity(0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.onSecondary.withOpacity(0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.onSecondary.withOpacity(0.1); + } + } + if (toggleable) { + if (states.contains(WidgetState.pressed)) { + return _colors.onSecondaryContainer.withOpacity(0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.onSecondaryContainer.withOpacity(0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.onSecondaryContainer.withOpacity(0.1); + } + } + if (states.contains(WidgetState.pressed)) { + return _colors.onSecondaryContainer.withOpacity(0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.onSecondaryContainer.withOpacity(0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.onSecondaryContainer.withOpacity(0.1); + } + return Colors.transparent; + }); + + @override + WidgetStateProperty? get elevation => const MaterialStatePropertyAll(0.0); + + @override + WidgetStateProperty? get shadowColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get surfaceTintColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get padding => + MaterialStatePropertyAll(switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 6.0, 4.0, 6.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(6.0, 6.0, 6.0, 6.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(10.0, 6.0, 10.0, 6.0), + }, + ButtonSize.small => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 8.0, 4.0, 8.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(8.0, 8.0, 8.0, 8.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(14.0, 8.0, 14.0, 8.0), + }, + ButtonSize.medium => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(12.0, 16.0, 12.0, 16.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(16.0, 16.0, 16.0, 16.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(24.0, 16.0, 24.0, 16.0), + }, + ButtonSize.large => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(16.0, 32.0, 16.0, 32.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(32.0, 32.0, 32.0, 32.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(48.0, 32.0, 48.0, 32.0), + }, + ButtonSize.xLarge => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(32.0, 48.0, 32.0, 48.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(48.0, 48.0, 48.0, 48.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(72.0, 48.0, 72.0, 48.0), + }, + }); + + @override + WidgetStateProperty? get minimumSize => + MaterialStatePropertyAll(switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(28.0, 32.0), + IconButtonWidth.standard => const Size(32.0, 32.0), + IconButtonWidth.wide => const Size(40.0, 32.0), + }, + ButtonSize.small => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(32.0, 40.0), + IconButtonWidth.standard => const Size(40.0, 40.0), + IconButtonWidth.wide => const Size(52.0, 40.0), + }, + ButtonSize.medium => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(48.0, 56.0), + IconButtonWidth.standard => const Size(56.0, 56.0), + IconButtonWidth.wide => const Size(72.0, 56.0), + }, + ButtonSize.large => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(64.0, 96.0), + IconButtonWidth.standard => const Size(96.0, 96.0), + IconButtonWidth.wide => const Size(128.0, 96.0), + }, + ButtonSize.xLarge => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(104.0, 136.0), + IconButtonWidth.standard => const Size(136.0, 136.0), + IconButtonWidth.wide => const Size(184.0, 136.0), + }, + }); + + @override + WidgetStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); + + @override + WidgetStateProperty? get iconSize => + MaterialStatePropertyAll(switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => 20.0, + ButtonSize.small => 24.0, + ButtonSize.medium => 24.0, + ButtonSize.large => 32.0, + ButtonSize.xLarge => 40.0, + }); + + @override + WidgetStateProperty? get shape => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.pressed)) { + return switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSize.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSize.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSize.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSize.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + }; + } + if (toggleable && states.contains(WidgetState.selected)) { + return switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSize.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSize.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSize.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + ButtonSize.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + }; + } + return switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => const StadiumBorder(), + ButtonSize.small => const StadiumBorder(), + ButtonSize.medium => const StadiumBorder(), + ButtonSize.large => const StadiumBorder(), + ButtonSize.xLarge => const StadiumBorder(), + }; + }); + + @override + WidgetStateProperty? get side => null; + + @override + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; + + @override + VisualDensity? get visualDensity => VisualDensity.standard; + + @override + MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} + +class _M3EOutlinedIconButtonDefaults extends ButtonStyle { + _M3EOutlinedIconButtonDefaults(this.context, this.toggleable, this.buttonSize, this.buttonWidth) + : super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; + final ButtonSize? buttonSize; + final IconButtonWidth? buttonWidth; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + WidgetStateProperty? get backgroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + if (toggleable && states.contains(WidgetState.selected)) { + return _colors.onSurface.withOpacity(0.1); + } + return Colors.transparent; + } + if (toggleable && states.contains(WidgetState.selected)) { + return _colors.inverseSurface; + } + return Colors.transparent; + }); + + @override + WidgetStateProperty? get foregroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + if (toggleable && states.contains(WidgetState.selected)) { + return _colors.onInverseSurface; + } + return _colors.onSurfaceVariant; + }); + + @override + WidgetStateProperty? get overlayColor => + WidgetStateProperty.resolveWith((Set states) { + if (toggleable && states.contains(WidgetState.selected)) { + if (states.contains(WidgetState.pressed)) { + return _colors.onInverseSurface.withOpacity(0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.onInverseSurface.withOpacity(0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.onInverseSurface.withOpacity(0.1); + } + } + if (states.contains(WidgetState.pressed)) { + return _colors.onSurfaceVariant.withOpacity(0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.onSurfaceVariant.withOpacity(0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.onSurfaceVariant.withOpacity(0.1); + } + return Colors.transparent; + }); + + @override + WidgetStateProperty? get elevation => const MaterialStatePropertyAll(0.0); + + @override + WidgetStateProperty? get shadowColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get surfaceTintColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get padding => + MaterialStatePropertyAll(switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 6.0, 4.0, 6.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(6.0, 6.0, 6.0, 6.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(10.0, 6.0, 10.0, 6.0), + }, + ButtonSize.small => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 8.0, 4.0, 8.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(8.0, 8.0, 8.0, 8.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(14.0, 8.0, 14.0, 8.0), + }, + ButtonSize.medium => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(12.0, 16.0, 12.0, 16.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(16.0, 16.0, 16.0, 16.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(24.0, 16.0, 24.0, 16.0), + }, + ButtonSize.large => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(16.0, 32.0, 16.0, 32.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(32.0, 32.0, 32.0, 32.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(48.0, 32.0, 48.0, 32.0), + }, + ButtonSize.xLarge => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB(32.0, 48.0, 32.0, 48.0), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB(48.0, 48.0, 48.0, 48.0), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB(72.0, 48.0, 72.0, 48.0), + }, + }); + + @override + WidgetStateProperty? get minimumSize => + MaterialStatePropertyAll(switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(28.0, 32.0), + IconButtonWidth.standard => const Size(32.0, 32.0), + IconButtonWidth.wide => const Size(40.0, 32.0), + }, + ButtonSize.small => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(32.0, 40.0), + IconButtonWidth.standard => const Size(40.0, 40.0), + IconButtonWidth.wide => const Size(52.0, 40.0), + }, + ButtonSize.medium => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(48.0, 56.0), + IconButtonWidth.standard => const Size(56.0, 56.0), + IconButtonWidth.wide => const Size(72.0, 56.0), + }, + ButtonSize.large => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(64.0, 96.0), + IconButtonWidth.standard => const Size(96.0, 96.0), + IconButtonWidth.wide => const Size(128.0, 96.0), + }, + ButtonSize.xLarge => switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(104.0, 136.0), + IconButtonWidth.standard => const Size(136.0, 136.0), + IconButtonWidth.wide => const Size(184.0, 136.0), + }, + }); + + @override + WidgetStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); + + @override + WidgetStateProperty? get iconSize => + MaterialStatePropertyAll(switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => 20.0, + ButtonSize.small => 24.0, + ButtonSize.medium => 24.0, + ButtonSize.large => 32.0, + ButtonSize.xLarge => 40.0, + }); + + @override + WidgetStateProperty? get shape => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.pressed)) { + return switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSize.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSize.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSize.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSize.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + }; + } + if (toggleable && states.contains(WidgetState.selected)) { + return switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSize.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSize.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSize.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + ButtonSize.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + }; + } + return switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => const StadiumBorder(), + ButtonSize.small => const StadiumBorder(), + ButtonSize.medium => const StadiumBorder(), + ButtonSize.large => const StadiumBorder(), + ButtonSize.xLarge => const StadiumBorder(), + }; + }); + + @override + WidgetStateProperty? get side => + WidgetStateProperty.resolveWith((Set states) { + if (toggleable && states.contains(WidgetState.selected)) { + return null; + } + if (states.contains(WidgetState.disabled)) { + return BorderSide( + color: _colors.outlineVariant, + width: switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => 1.0, + ButtonSize.small => 1.0, + ButtonSize.medium => 1.0, + ButtonSize.large => 2.0, + ButtonSize.xLarge => 3.0, + }, + ); + } + return BorderSide( + color: _colors.outlineVariant, + width: switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => 1.0, + ButtonSize.small => 1.0, + ButtonSize.medium => 1.0, + ButtonSize.large => 2.0, + ButtonSize.xLarge => 3.0, + }, + ); + }); + + @override + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; + + @override + VisualDensity? get visualDensity => VisualDensity.standard; + + @override + MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} diff --git a/packages/material_ui/lib/src/icon_button.dart b/packages/material_ui/lib/src/icon_button.dart index d093d3686467..78692f13fd76 100644 --- a/packages/material_ui/lib/src/icon_button.dart +++ b/packages/material_ui/lib/src/icon_button.dart @@ -31,6 +31,8 @@ import 'theme.dart'; import 'theme_data.dart'; import 'tooltip.dart'; +part 'generated/icon_button_m3e_defaults.g.dart'; + // Examples can assume: // late BuildContext context; @@ -626,6 +628,10 @@ class IconButton extends StatelessWidget { /// create a [WidgetStateProperty] with a single value for all /// states. /// + /// The [size] and [iconButtonWidth] parameters configure the Material 3 + /// Expressive token size and width through [ButtonStyle.size] and + /// [ButtonStyle.iconButtonWidth]. + /// /// All parameters default to null, by default this method returns /// a [ButtonStyle] that doesn't override anything. /// @@ -670,6 +676,8 @@ class IconButton extends StatelessWidget { bool? enableFeedback, AlignmentGeometry? alignment, InteractiveInkFeatureFactory? splashFactory, + ButtonSize? size, + IconButtonWidth? iconButtonWidth, }) { final Color? overlayFallback = overlayColor ?? foregroundColor; WidgetStateProperty? overlayColorProp; @@ -710,6 +718,8 @@ class IconButton extends StatelessWidget { enableFeedback: enableFeedback, alignment: alignment, splashFactory: splashFactory, + size: size, + iconButtonWidth: iconButtonWidth, ); } @@ -719,7 +729,6 @@ class IconButton extends StatelessWidget { if (theme.useMaterial3) { final StyleVariant effectiveVariant = IconButtonTheme.of(context).variant ?? theme.variant; - assert(effectiveVariant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); final Size? minSize = constraints == null ? null @@ -764,6 +773,7 @@ class IconButton extends StatelessWidget { focusNode: focusNode, isSelected: isSelected, variant: _variant, + styleVariant: effectiveVariant, tooltip: tooltip, statesController: statesController, child: effectiveIcon, @@ -870,6 +880,7 @@ class _SelectableIconButton extends StatefulWidget { this.onHover, this.statesController, required this.variant, + required this.styleVariant, required this.autofocus, required this.onPressed, this.tooltip, @@ -880,6 +891,7 @@ class _SelectableIconButton extends StatefulWidget { final ButtonStyle? style; final FocusNode? focusNode; final _IconButtonVariant variant; + final StyleVariant styleVariant; final bool autofocus; final VoidCallback? onPressed; final String? tooltip; @@ -948,7 +960,8 @@ class _SelectableIconButtonState extends State<_SelectableIconButton> { onPressed: widget.onPressed, onHover: widget.onHover, onLongPress: widget.onPressed != null ? widget.onLongPress : null, - variant: widget.variant, + iconButtonVariant: widget.variant, + styleVariant: widget.styleVariant, toggleable: toggleable, tooltip: widget.tooltip, child: Semantics(selected: widget.isSelected, child: widget.child), @@ -971,15 +984,25 @@ class _IconButtonM3 extends ButtonStyleButton { super.onLongPress, super.autofocus = false, super.statesController, - required this.variant, + required this.iconButtonVariant, + required this.styleVariant, required this.toggleable, super.tooltip, required Widget super.child, }) : super(onFocusChange: null, clipBehavior: Clip.none); - final _IconButtonVariant variant; + final _IconButtonVariant iconButtonVariant; + final StyleVariant styleVariant; final bool toggleable; + ButtonSize? _effectiveSize(BuildContext context) { + return style?.size ?? IconButtonTheme.of(context).style?.size; + } + + IconButtonWidth? _effectiveWidth(BuildContext context) { + return style?.iconButtonWidth ?? IconButtonTheme.of(context).style?.iconButtonWidth; + } + /// ## Material 3 defaults /// /// If [ThemeData.useMaterial3] is set to true the following defaults will @@ -1017,11 +1040,42 @@ class _IconButtonM3 extends ButtonStyleButton { /// * `splashFactory` - Theme.splashFactory @override ButtonStyle defaultStyleOf(BuildContext context) { - return switch (variant) { - _IconButtonVariant.filled => _FilledIconButtonDefaultsM3(context, toggleable), - _IconButtonVariant.filledTonal => _FilledTonalIconButtonDefaultsM3(context, toggleable), - _IconButtonVariant.outlined => _OutlinedIconButtonDefaultsM3(context, toggleable), - _IconButtonVariant.standard => _IconButtonDefaultsM3(context, toggleable), + final ButtonSize? effectiveSize = _effectiveSize(context); + final IconButtonWidth? effectiveWidth = _effectiveWidth(context); + + return switch (styleVariant) { + StyleVariant.material3 => switch (iconButtonVariant) { + _IconButtonVariant.filled => _FilledIconButtonDefaultsM3(context, toggleable), + _IconButtonVariant.filledTonal => _FilledTonalIconButtonDefaultsM3(context, toggleable), + _IconButtonVariant.outlined => _OutlinedIconButtonDefaultsM3(context, toggleable), + _IconButtonVariant.standard => _IconButtonDefaultsM3(context, toggleable), + }, + StyleVariant.material3Expressive => switch (iconButtonVariant) { + _IconButtonVariant.filled => _M3EFilledIconButtonDefaults( + context, + toggleable, + effectiveSize, + effectiveWidth, + ), + _IconButtonVariant.filledTonal => _M3EFilledTonalIconButtonDefaults( + context, + toggleable, + effectiveSize, + effectiveWidth, + ), + _IconButtonVariant.outlined => _M3EOutlinedIconButtonDefaults( + context, + toggleable, + effectiveSize, + effectiveWidth, + ), + _IconButtonVariant.standard => _M3EIconButtonDefaults( + context, + toggleable, + effectiveSize, + effectiveWidth, + ), + }, }; } diff --git a/packages/material_ui/lib/src/icon_button_theme.dart b/packages/material_ui/lib/src/icon_button_theme.dart index e20a5d27be3c..7f8713fc79d0 100644 --- a/packages/material_ui/lib/src/icon_button_theme.dart +++ b/packages/material_ui/lib/src/icon_button_theme.dart @@ -9,7 +9,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'button_style.dart'; -import 'debug.dart'; import 'theme.dart'; // Examples can assume: @@ -40,8 +39,7 @@ class IconButtonThemeData with Diagnosticable { /// Creates a [IconButtonThemeData]. /// /// The [style] may be null. - const IconButtonThemeData({this.style, this.variant}) - : assert(variant != .material3Expressive, kUnsupportedStyleVariantAssertionMessage); + const IconButtonThemeData({this.style, this.variant}); /// Overrides for [IconButton]'s default style if [ThemeData.useMaterial3] /// is set to true. diff --git a/packages/material_ui/test/icon_button_theme_test.dart b/packages/material_ui/test/icon_button_theme_test.dart index 6ad4e03ad603..0abc6a1957c2 100644 --- a/packages/material_ui/test/icon_button_theme_test.dart +++ b/packages/material_ui/test/icon_button_theme_test.dart @@ -7,13 +7,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:material_ui/material_ui.dart'; -class _IconButtonThemeDataWithExpressiveVariant extends IconButtonThemeData { - const _IconButtonThemeDataWithExpressiveVariant(); - - @override - StyleVariant? get variant => .material3Expressive; -} - void main() { RenderObject getOverlayColor(WidgetTester tester) { return tester.allRenderObjects.firstWhere( @@ -27,26 +20,28 @@ void main() { expect(identical(IconButtonThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('IconButton asserts on unsupported style variants', (WidgetTester tester) async { + test('IconButtonThemeData supports Material 3 Expressive variant', () { + const data = IconButtonThemeData(variant: StyleVariant.material3Expressive); + + expect(data.variant, StyleVariant.material3Expressive); + }); + + testWidgets('IconButton supports Material 3 Expressive style variants', ( + WidgetTester tester, + ) async { await tester.pumpWidget( - const MaterialApp( - home: Scaffold( + MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold( body: IconButtonTheme( - data: _IconButtonThemeDataWithExpressiveVariant(), + data: IconButtonThemeData(variant: StyleVariant.material3Expressive), child: IconButton(onPressed: null, icon: Icon(Icons.ac_unit)), ), ), ), ); - expect( - tester.takeException(), - isA().having( - (AssertionError error) => error.message, - 'message', - kUnsupportedStyleVariantAssertionMessage, - ), - ); + expect(tester.takeException(), isNull); }); testWidgets('Passing no IconButtonTheme returns defaults', (WidgetTester tester) async { diff --git a/packages/material_ui/test/material_3_expressive_icon_button_test.dart b/packages/material_ui/test/material_3_expressive_icon_button_test.dart new file mode 100644 index 000000000000..776c273c6ad8 --- /dev/null +++ b/packages/material_ui/test/material_3_expressive_icon_button_test.dart @@ -0,0 +1,655 @@ +// Copyright 2013 The Flutter Authors +// 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_test/flutter_test.dart'; +import 'package:material_ui/material_ui.dart'; + +void main() { + // Helper to create a testable icon button. + Widget buildApp({required Widget child, ThemeData? theme}) { + return MaterialApp( + theme: + theme ?? + ThemeData( + useMaterial3: true, + iconButtonTheme: const IconButtonThemeData(variant: StyleVariant.material3Expressive), + ), + home: Scaffold(body: Center(child: child)), + ); + } + + Finder iconButtonMaterialFinder() { + return find.descendant(of: find.byType(IconButton), matching: find.byType(Material)); + } + + Material iconButtonMaterial(WidgetTester tester) { + return tester.widget(iconButtonMaterialFinder()); + } + + Size iconButtonMaterialSize(WidgetTester tester) { + return tester.getSize(iconButtonMaterialFinder()); + } + + ColorScheme colorScheme(WidgetTester tester) { + return Theme.of(tester.element(find.byType(IconButton))).colorScheme; + } + + Color? iconColor(WidgetTester tester, IconData icon) { + return IconTheme.of(tester.element(find.byIcon(icon))).color; + } + + group('M3E IconButton size variants', () { + testWidgets('default size is small (40x40)', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButton(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + // ButtonStyleButton renders with minimum size 40x40, but tap target + // padding brings it to 48x48. + expect(iconButtonMaterialSize(tester), const Size(40.0, 40.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(48.0, 48.0)); + }); + + testWidgets('xSmall size renders at 32dp minimum', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(size: ButtonSize.xSmall), + ), + ), + ); + + expect(iconButtonMaterialSize(tester), const Size(32.0, 32.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(48.0, 48.0)); + }); + + testWidgets('styleFrom sets the size variant', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: IconButton.styleFrom(size: ButtonSize.medium), + ), + ), + ); + + expect(iconButtonMaterialSize(tester), const Size(56.0, 56.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(56.0, 56.0)); + }); + + testWidgets('medium size renders at 56dp minimum', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(size: ButtonSize.medium), + ), + ), + ); + + expect(iconButtonMaterialSize(tester), const Size(56.0, 56.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(56.0, 56.0)); + }); + + testWidgets('large size renders at 96dp minimum', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(size: ButtonSize.large), + ), + ), + ); + + expect(iconButtonMaterialSize(tester), const Size(96.0, 96.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(96.0, 96.0)); + }); + + testWidgets('xLarge size renders at 136dp minimum', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(size: ButtonSize.xLarge), + ), + ), + ); + + expect(iconButtonMaterialSize(tester), const Size(136.0, 136.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(136.0, 136.0)); + }); + }); + + group('M3E IconButton width variants', () { + testWidgets('small IconButton supports narrow, standard, and wide widths', ( + WidgetTester tester, + ) async { + Future materialSizeFor(IconButtonWidth width) async { + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: ButtonStyle(iconButtonWidth: width), + ), + ), + ); + return iconButtonMaterialSize(tester); + } + + expect(await materialSizeFor(IconButtonWidth.narrow), const Size(32.0, 40.0)); + expect(await materialSizeFor(IconButtonWidth.standard), const Size(40.0, 40.0)); + expect(await materialSizeFor(IconButtonWidth.wide), const Size(52.0, 40.0)); + + expect(iconButtonMaterial(tester).animationDuration, kThemeChangeDuration); + }); + + testWidgets('IconButtonThemeData style width sets default width', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + theme: ThemeData( + useMaterial3: true, + iconButtonTheme: const IconButtonThemeData( + style: ButtonStyle(iconButtonWidth: IconButtonWidth.wide), + variant: StyleVariant.material3Expressive, + ), + ), + child: IconButton(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + expect(iconButtonMaterialSize(tester), const Size(52.0, 40.0)); + }); + }); + + group('M3E IconButton shape', () { + OutlinedBorder materialShape(WidgetTester tester) { + final Material material = tester.widget( + find.descendant(of: find.byType(IconButton), matching: find.byType(Material)), + ); + return material.shape! as OutlinedBorder; + } + + testWidgets('default shape resolves M3E token shapes by state', (WidgetTester tester) async { + final statesController = MaterialStatesController(); + await tester.pumpWidget( + buildApp( + child: IconButton( + statesController: statesController, + isSelected: true, + onPressed: () {}, + icon: const Icon(Icons.add), + ), + ), + ); + expect( + materialShape(tester), + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0))), + ); + + statesController.update(WidgetState.pressed, true); + await tester.pumpAndSettle(); + + expect( + materialShape(tester), + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))), + ); + statesController.dispose(); + }); + + testWidgets('ButtonStyle.shape remains the stateful shape override API', ( + WidgetTester tester, + ) async { + final statesController = MaterialStatesController(); + await tester.pumpWidget( + buildApp( + child: IconButton( + statesController: statesController, + onPressed: () {}, + icon: const Icon(Icons.add), + style: ButtonStyle( + shape: WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.pressed)) { + return const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ); + } + return const StadiumBorder(); + }), + ), + ), + ), + ); + + expect(materialShape(tester), const StadiumBorder()); + + statesController.update(WidgetState.pressed, true); + await tester.pumpAndSettle(); + + expect( + materialShape(tester), + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))), + ); + statesController.dispose(); + }); + }); + + group('M3E IconButton variants', () { + testWidgets('standard variant has transparent background', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButton(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + expect(iconButtonMaterial(tester).color, Colors.transparent); + expect(iconButtonMaterial(tester).shape, const StadiumBorder()); + }); + + testWidgets('filled variant resolves default container color', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButton.filled(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + expect(iconButtonMaterial(tester).color, colorScheme(tester).primary); + expect(iconButtonMaterial(tester).shape, const StadiumBorder()); + }); + + testWidgets('filledTonal variant resolves default container color', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + buildApp( + child: IconButton.filledTonal(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + expect(iconButtonMaterial(tester).color, colorScheme(tester).secondaryContainer); + expect(iconButtonMaterial(tester).shape, const StadiumBorder()); + }); + + testWidgets('outlined variant resolves default side and transparent background', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + buildApp( + child: IconButton.outlined(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + final shape = iconButtonMaterial(tester).shape! as StadiumBorder; + expect(iconButtonMaterial(tester).color, Colors.transparent); + expect(shape.side, BorderSide(color: colorScheme(tester).outlineVariant)); + }); + + testWidgets('filled variant with style size', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButton.filled( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(size: ButtonSize.large), + ), + ), + ); + + expect(iconButtonMaterialSize(tester), const Size(96.0, 96.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(96.0, 96.0)); + }); + + testWidgets('outlined variant with style size', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButton.outlined( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(size: ButtonSize.medium), + ), + ), + ); + + expect(iconButtonMaterialSize(tester), const Size(56.0, 56.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(56.0, 56.0)); + }); + }); + + group('M3E IconButton theme integration', () { + testWidgets('IconButtonThemeData style size sets default size', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + theme: ThemeData( + useMaterial3: true, + iconButtonTheme: const IconButtonThemeData( + style: ButtonStyle(size: ButtonSize.large), + variant: StyleVariant.material3Expressive, + ), + ), + child: IconButton(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + expect(iconButtonMaterialSize(tester), const Size(96.0, 96.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(96.0, 96.0)); + }); + + testWidgets('widget size overrides theme size', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + theme: ThemeData( + useMaterial3: true, + iconButtonTheme: const IconButtonThemeData( + style: ButtonStyle(size: ButtonSize.large), + variant: StyleVariant.material3Expressive, + ), + ), + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(size: ButtonSize.xSmall), + ), + ), + ); + + expect(iconButtonMaterialSize(tester), const Size(32.0, 32.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(48.0, 48.0)); + }); + + testWidgets('IconButtonTheme wrapping sets size', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButtonTheme( + data: const IconButtonThemeData( + style: ButtonStyle(size: ButtonSize.medium), + variant: StyleVariant.material3Expressive, + ), + child: IconButton(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ), + ); + + expect(iconButtonMaterialSize(tester), const Size(56.0, 56.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(56.0, 56.0)); + }); + }); + + group('M3E IconButton selection', () { + testWidgets('isSelected shows selectedIcon', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + isSelected: true, + icon: const Icon(Icons.favorite_border), + selectedIcon: const Icon(Icons.favorite), + ), + ), + ); + + expect(find.byIcon(Icons.favorite), findsOneWidget); + expect(find.byIcon(Icons.favorite_border), findsNothing); + }); + + testWidgets('isSelected exposes selected semantics', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + isSelected: true, + icon: const Icon(Icons.favorite_border, semanticLabel: 'favorite'), + selectedIcon: const Icon(Icons.favorite, semanticLabel: 'favorite'), + ), + ), + ); + + expect( + tester.getSemantics(find.byType(IconButton)), + matchesSemantics( + hasTapAction: true, + hasFocusAction: true, + hasEnabledState: true, + isButton: true, + isEnabled: true, + isFocusable: true, + hasSelectedState: true, + isSelected: true, + label: 'favorite', + ), + ); + handle.dispose(); + }); + + testWidgets('external selected state does not affect non-toggleable visual state', ( + WidgetTester tester, + ) async { + final statesController = MaterialStatesController(); + statesController.update(WidgetState.selected, true); + + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + statesController: statesController, + icon: const Icon(Icons.favorite_border), + selectedIcon: const Icon(Icons.favorite), + ), + ), + ); + + final Material material = tester.widget( + find.descendant(of: find.byType(IconButton), matching: find.byType(Material)), + ); + expect(material.shape, const StadiumBorder()); + expect(find.byIcon(Icons.favorite_border), findsOneWidget); + expect(find.byIcon(Icons.favorite), findsNothing); + }); + + testWidgets('isSelected false shows regular icon', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + isSelected: false, + icon: const Icon(Icons.favorite_border), + selectedIcon: const Icon(Icons.favorite), + ), + ), + ); + + expect(find.byIcon(Icons.favorite_border), findsOneWidget); + expect(find.byIcon(Icons.favorite), findsNothing); + }); + + testWidgets('isSelected updates selected widget state when toggled through null', ( + WidgetTester tester, + ) async { + final statesController = MaterialStatesController(); + + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + isSelected: true, + statesController: statesController, + icon: const Icon(Icons.favorite_border), + selectedIcon: const Icon(Icons.favorite), + ), + ), + ); + expect(statesController.value, contains(WidgetState.selected)); + + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + statesController: statesController, + icon: const Icon(Icons.favorite_border), + selectedIcon: const Icon(Icons.favorite), + ), + ), + ); + expect(statesController.value, isNot(contains(WidgetState.selected))); + + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + isSelected: false, + statesController: statesController, + icon: const Icon(Icons.favorite_border), + selectedIcon: const Icon(Icons.favorite), + ), + ), + ); + expect(statesController.value, isNot(contains(WidgetState.selected))); + + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + isSelected: true, + statesController: statesController, + icon: const Icon(Icons.favorite_border), + selectedIcon: const Icon(Icons.favorite), + ), + ), + ); + expect(statesController.value, contains(WidgetState.selected)); + }); + }); + + group('M3E IconButton disabled state', () { + testWidgets('disabled button has reduced opacity colors', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp(child: const IconButton(onPressed: null, icon: Icon(Icons.add))), + ); + + expect(iconButtonMaterial(tester).color, Colors.transparent); + expect(iconColor(tester, Icons.add), colorScheme(tester).onSurface.withOpacity(0.38)); + }); + + testWidgets('onLongPress without onPressed keeps button disabled', (WidgetTester tester) async { + var longPressed = false; + final SemanticsHandle handle = tester.ensureSemantics(); + + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: null, + onLongPress: () { + longPressed = true; + }, + icon: const Icon(Icons.add, semanticLabel: 'add'), + ), + ), + ); + + expect( + tester.getSemantics(find.byType(IconButton)), + matchesSemantics(hasEnabledState: true, isButton: true, label: 'add'), + ); + + await tester.longPress(find.byType(IconButton)); + expect(longPressed, isFalse); + handle.dispose(); + }); + + testWidgets('disabled filled button has reduced background', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp(child: const IconButton.filled(onPressed: null, icon: Icon(Icons.add))), + ); + + expect(iconButtonMaterial(tester).color, colorScheme(tester).onSurface.withOpacity(0.1)); + expect(iconColor(tester, Icons.add), colorScheme(tester).onSurface.withOpacity(0.38)); + }); + }); + + group('IconButtonThemeData', () { + test('equality', () { + const a = IconButtonThemeData( + style: ButtonStyle(size: ButtonSize.small, iconButtonWidth: IconButtonWidth.standard), + ); + const b = IconButtonThemeData( + style: ButtonStyle(size: ButtonSize.small, iconButtonWidth: IconButtonWidth.standard), + ); + const c = IconButtonThemeData( + style: ButtonStyle(size: ButtonSize.large, iconButtonWidth: IconButtonWidth.wide), + ); + + expect(a, equals(b)); + expect(a, isNot(equals(c))); + }); + + test('hashCode', () { + const a = IconButtonThemeData( + style: ButtonStyle(size: ButtonSize.small, iconButtonWidth: IconButtonWidth.narrow), + ); + const b = IconButtonThemeData( + style: ButtonStyle(size: ButtonSize.small, iconButtonWidth: IconButtonWidth.narrow), + ); + + expect(a.hashCode, equals(b.hashCode)); + }); + + test('lerp', () { + const a = IconButtonThemeData( + style: ButtonStyle(size: ButtonSize.small, iconButtonWidth: IconButtonWidth.narrow), + ); + const b = IconButtonThemeData( + style: ButtonStyle(size: ButtonSize.large, iconButtonWidth: IconButtonWidth.wide), + ); + + expect(IconButtonThemeData.lerp(a, b, 0.0)?.style?.size, ButtonSize.small); + expect(IconButtonThemeData.lerp(a, b, 0.4)?.style?.size, ButtonSize.small); + expect(IconButtonThemeData.lerp(a, b, 0.5)?.style?.size, ButtonSize.large); + expect(IconButtonThemeData.lerp(a, b, 1.0)?.style?.size, ButtonSize.large); + expect(IconButtonThemeData.lerp(a, b, 0.4)?.style?.iconButtonWidth, IconButtonWidth.narrow); + expect(IconButtonThemeData.lerp(a, b, 0.5)?.style?.iconButtonWidth, IconButtonWidth.wide); + }); + + test('debugFillProperties includes size and width', () { + const data = IconButtonThemeData( + style: ButtonStyle(size: ButtonSize.medium, iconButtonWidth: IconButtonWidth.wide), + ); + final builder = DiagnosticPropertiesBuilder(); + data.debugFillProperties(builder); + + final List descriptions = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(descriptions, contains(contains('size: medium'))); + expect(descriptions, contains(contains('iconButtonWidth: wide'))); + }); + }); + + group('M3E IconButton variant opt in', () { + testWidgets('IconButtonThemeData variant enables M3E defaults', (WidgetTester tester) async { + await tester.pumpWidget( + buildApp( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(size: ButtonSize.medium), + ), + ), + ); + + expect(iconButtonMaterialSize(tester), const Size(56.0, 56.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(56.0, 56.0)); + }); + }); +} diff --git a/packages/material_ui/test/theme_data_test.dart b/packages/material_ui/test/theme_data_test.dart index 4f1517b3e02a..4ec3fd5fec20 100644 --- a/packages/material_ui/test/theme_data_test.dart +++ b/packages/material_ui/test/theme_data_test.dart @@ -97,7 +97,6 @@ void main() { () => ThemeData( floatingActionButtonTheme: FloatingActionButtonThemeData(variant: .material3Expressive), ), - () => ThemeData(iconButtonTheme: IconButtonThemeData(variant: .material3Expressive)), () => ThemeData(menuButtonTheme: MenuButtonThemeData(variant: .material3Expressive)), () => ThemeData(menuTheme: MenuThemeData(variant: .material3Expressive)), () => ThemeData(navigationBarTheme: NavigationBarThemeData(variant: .material3Expressive)), diff --git a/packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart b/packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart index 2da295bc4e99..38c5fdd7b8aa 100644 --- a/packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart +++ b/packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart @@ -12,6 +12,8 @@ import 'package:args/args.dart'; +import '../templates/icon_button_template.dart'; + // TODO(elliette): Import template files. // import '../templates/x_template.dart'; @@ -23,6 +25,5 @@ Future main(List args) async { // TODO(elliette): Add token logger when verbose flag is used. // ignore: unused_local_variable final verbose = argResults['verbose'] as bool; - // TODO(elliette): Invoke template generators. - // const XTemplate().generateFile(verbose: verbose); + const IconButtonTemplate().generateFile(verbose: verbose); } diff --git a/packages/material_ui/tool/gen_defaults/templates/icon_button_template.dart b/packages/material_ui/tool/gen_defaults/templates/icon_button_template.dart new file mode 100644 index 000000000000..3b99de58d03a --- /dev/null +++ b/packages/material_ui/tool/gen_defaults/templates/icon_button_template.dart @@ -0,0 +1,697 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../data/icon_button_filled.dart'; +import '../data/icon_button_large.dart'; +import '../data/icon_button_medium.dart'; +import '../data/icon_button_outlined.dart'; +import '../data/icon_button_small.dart'; +import '../data/icon_button_standard.dart'; +import '../data/icon_button_tonal.dart'; +import '../data/icon_button_xlarge.dart'; +import '../data/icon_button_xsmall.dart'; +import 'template.dart'; + +class IconButtonTemplate extends M3ETokenTemplate { + const IconButtonTemplate(); + + @override + String get name => 'Icon Button'; + + @override + String get parentFilePath => 'icon_button.dart'; + + @override + String generateContents(String className) { + return ''' +${_generateStandardDefaults(className)} +${_generateFilledDefaults()} +${_generateFilledTonalDefaults()} +${_generateOutlinedDefaults()} +'''; + } + + String _sizeSwitch({ + required String xSmall, + required String small, + required String medium, + required String large, + required String xLarge, + }) { + return ''' +switch (buttonSize ?? ButtonSize.small) { + ButtonSize.xSmall => $xSmall, + ButtonSize.small => $small, + ButtonSize.medium => $medium, + ButtonSize.large => $large, + ButtonSize.xLarge => $xLarge, + }'''; + } + + String get _paddingSwitch { + return _sizeSwitch( + xSmall: _edgeInsetsSwitch( + defaultLeading: TokenIconButtonXsmall.defaultLeadingSpace, + defaultTrailing: TokenIconButtonXsmall.defaultTrailingSpace, + narrowLeading: TokenIconButtonXsmall.narrowLeadingSpace, + narrowTrailing: TokenIconButtonXsmall.narrowTrailingSpace, + wideLeading: TokenIconButtonXsmall.wideLeadingSpace, + wideTrailing: TokenIconButtonXsmall.wideTrailingSpace, + ), + small: _edgeInsetsSwitch( + defaultLeading: TokenIconButtonSmall.defaultLeadingSpace, + defaultTrailing: TokenIconButtonSmall.defaultTrailingSpace, + narrowLeading: TokenIconButtonSmall.narrowLeadingSpace, + narrowTrailing: TokenIconButtonSmall.narrowTrailingSpace, + wideLeading: TokenIconButtonSmall.wideLeadingSpace, + wideTrailing: TokenIconButtonSmall.wideTrailingSpace, + ), + medium: _edgeInsetsSwitch( + defaultLeading: TokenIconButtonMedium.defaultLeadingSpace, + defaultTrailing: TokenIconButtonMedium.defaultTrailingSpace, + narrowLeading: TokenIconButtonMedium.narrowLeadingSpace, + narrowTrailing: TokenIconButtonMedium.narrowTrailingSpace, + wideLeading: TokenIconButtonMedium.wideLeadingSpace, + wideTrailing: TokenIconButtonMedium.wideTrailingSpace, + ), + large: _edgeInsetsSwitch( + defaultLeading: TokenIconButtonLarge.defaultLeadingSpace, + defaultTrailing: TokenIconButtonLarge.defaultTrailingSpace, + narrowLeading: TokenIconButtonLarge.narrowLeadingSpace, + narrowTrailing: TokenIconButtonLarge.narrowTrailingSpace, + wideLeading: TokenIconButtonLarge.wideLeadingSpace, + wideTrailing: TokenIconButtonLarge.wideTrailingSpace, + ), + xLarge: _edgeInsetsSwitch( + defaultLeading: TokenIconButtonXlarge.defaultLeadingSpace, + defaultTrailing: TokenIconButtonXlarge.defaultTrailingSpace, + narrowLeading: TokenIconButtonXlarge.narrowLeadingSpace, + narrowTrailing: TokenIconButtonXlarge.narrowTrailingSpace, + wideLeading: TokenIconButtonXlarge.wideLeadingSpace, + wideTrailing: TokenIconButtonXlarge.wideTrailingSpace, + ), + ); + } + + String _edgeInsetsSwitch({ + required double defaultLeading, + required double defaultTrailing, + required double narrowLeading, + required double narrowTrailing, + required double wideLeading, + required double wideTrailing, + }) { + return ''' +switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const EdgeInsetsDirectional.fromSTEB($narrowLeading, $defaultLeading, $narrowTrailing, $defaultTrailing), + IconButtonWidth.standard => const EdgeInsetsDirectional.fromSTEB($defaultLeading, $defaultLeading, $defaultTrailing, $defaultTrailing), + IconButtonWidth.wide => const EdgeInsetsDirectional.fromSTEB($wideLeading, $defaultLeading, $wideTrailing, $defaultTrailing), + }'''; + } + + String get _minimumSizeSwitch { + return _sizeSwitch( + xSmall: _minimumSizeWidthSwitch( + iconSize: TokenIconButtonXsmall.iconSize, + height: TokenIconButtonXsmall.containerHeight, + defaultLeading: TokenIconButtonXsmall.defaultLeadingSpace, + defaultTrailing: TokenIconButtonXsmall.defaultTrailingSpace, + narrowLeading: TokenIconButtonXsmall.narrowLeadingSpace, + narrowTrailing: TokenIconButtonXsmall.narrowTrailingSpace, + wideLeading: TokenIconButtonXsmall.wideLeadingSpace, + wideTrailing: TokenIconButtonXsmall.wideTrailingSpace, + ), + small: _minimumSizeWidthSwitch( + iconSize: TokenIconButtonSmall.iconSize, + height: TokenIconButtonSmall.containerHeight, + defaultLeading: TokenIconButtonSmall.defaultLeadingSpace, + defaultTrailing: TokenIconButtonSmall.defaultTrailingSpace, + narrowLeading: TokenIconButtonSmall.narrowLeadingSpace, + narrowTrailing: TokenIconButtonSmall.narrowTrailingSpace, + wideLeading: TokenIconButtonSmall.wideLeadingSpace, + wideTrailing: TokenIconButtonSmall.wideTrailingSpace, + ), + medium: _minimumSizeWidthSwitch( + iconSize: TokenIconButtonMedium.iconSize, + height: TokenIconButtonMedium.containerHeight, + defaultLeading: TokenIconButtonMedium.defaultLeadingSpace, + defaultTrailing: TokenIconButtonMedium.defaultTrailingSpace, + narrowLeading: TokenIconButtonMedium.narrowLeadingSpace, + narrowTrailing: TokenIconButtonMedium.narrowTrailingSpace, + wideLeading: TokenIconButtonMedium.wideLeadingSpace, + wideTrailing: TokenIconButtonMedium.wideTrailingSpace, + ), + large: _minimumSizeWidthSwitch( + iconSize: TokenIconButtonLarge.iconSize, + height: TokenIconButtonLarge.containerHeight, + defaultLeading: TokenIconButtonLarge.defaultLeadingSpace, + defaultTrailing: TokenIconButtonLarge.defaultTrailingSpace, + narrowLeading: TokenIconButtonLarge.narrowLeadingSpace, + narrowTrailing: TokenIconButtonLarge.narrowTrailingSpace, + wideLeading: TokenIconButtonLarge.wideLeadingSpace, + wideTrailing: TokenIconButtonLarge.wideTrailingSpace, + ), + xLarge: _minimumSizeWidthSwitch( + iconSize: TokenIconButtonXlarge.iconSize, + height: TokenIconButtonXlarge.containerHeight, + defaultLeading: TokenIconButtonXlarge.defaultLeadingSpace, + defaultTrailing: TokenIconButtonXlarge.defaultTrailingSpace, + narrowLeading: TokenIconButtonXlarge.narrowLeadingSpace, + narrowTrailing: TokenIconButtonXlarge.narrowTrailingSpace, + wideLeading: TokenIconButtonXlarge.wideLeadingSpace, + wideTrailing: TokenIconButtonXlarge.wideTrailingSpace, + ), + ); + } + + String _minimumSizeWidthSwitch({ + required double iconSize, + required double height, + required double defaultLeading, + required double defaultTrailing, + required double narrowLeading, + required double narrowTrailing, + required double wideLeading, + required double wideTrailing, + }) { + return ''' +switch (buttonWidth ?? IconButtonWidth.standard) { + IconButtonWidth.narrow => const Size(${iconSize + narrowLeading + narrowTrailing}, $height), + IconButtonWidth.standard => const Size(${iconSize + defaultLeading + defaultTrailing}, $height), + IconButtonWidth.wide => const Size(${iconSize + wideLeading + wideTrailing}, $height), + }'''; + } + + String get _iconSizeSwitch { + return _sizeSwitch( + xSmall: '${TokenIconButtonXsmall.iconSize}', + small: '${TokenIconButtonSmall.iconSize}', + medium: '${TokenIconButtonMedium.iconSize}', + large: '${TokenIconButtonLarge.iconSize}', + xLarge: '${TokenIconButtonXlarge.iconSize}', + ); + } + + String get _outlineWidthSwitch { + return _sizeSwitch( + xSmall: '${TokenIconButtonXsmall.outlinedOutlineWidth}', + small: '${TokenIconButtonSmall.outlinedOutlineWidth}', + medium: '${TokenIconButtonMedium.outlinedOutlineWidth}', + large: '${TokenIconButtonLarge.outlinedOutlineWidth}', + xLarge: '${TokenIconButtonXlarge.outlinedOutlineWidth}', + ); + } + + String get _containerShapeSwitch { + return _sizeSwitch( + xSmall: shape(TokenIconButtonXsmall.containerShapeRound), + small: shape(TokenIconButtonSmall.containerShapeRound), + medium: shape(TokenIconButtonMedium.containerShapeRound), + large: shape(TokenIconButtonLarge.containerShapeRound), + xLarge: shape(TokenIconButtonXlarge.containerShapeRound), + ); + } + + String get _pressedShapeSwitch { + return _sizeSwitch( + xSmall: shape(TokenIconButtonXsmall.pressedContainerShape), + small: shape(TokenIconButtonSmall.pressedContainerShape), + medium: shape(TokenIconButtonMedium.pressedContainerShape), + large: shape(TokenIconButtonLarge.pressedContainerShape), + xLarge: shape(TokenIconButtonXlarge.pressedContainerShape), + ); + } + + String get _selectedShapeSwitch { + return _sizeSwitch( + xSmall: shape(TokenIconButtonXsmall.selectedContainerShapeRound), + small: shape(TokenIconButtonSmall.selectedContainerShapeRound), + medium: shape(TokenIconButtonMedium.selectedContainerShapeRound), + large: shape(TokenIconButtonLarge.selectedContainerShapeRound), + xLarge: shape(TokenIconButtonXlarge.selectedContainerShapeRound), + ); + } + + String get _sizeDependentProperties { + return ''' + @override + WidgetStateProperty? get padding => + MaterialStatePropertyAll($_paddingSwitch); + + @override + WidgetStateProperty? get minimumSize => + MaterialStatePropertyAll($_minimumSizeSwitch); + + @override + WidgetStateProperty? get maximumSize => + const MaterialStatePropertyAll(Size.infinite); + + @override + WidgetStateProperty? get iconSize => + MaterialStatePropertyAll($_iconSizeSwitch); + + @override + WidgetStateProperty? get shape => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.pressed)) { + return $_pressedShapeSwitch; + } + if (toggleable && states.contains(WidgetState.selected)) { + return $_selectedShapeSwitch; + } + return $_containerShapeSwitch; + }); +'''; + } + + String _generateStandardDefaults(String className) { + return ''' +class $className extends ButtonStyle { + $className(this.context, this.toggleable, this.buttonSize, this.buttonWidth) + : super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; + final ButtonSize? buttonSize; + final IconButtonWidth? buttonWidth; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + WidgetStateProperty? get backgroundColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get foregroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return ${componentColor(TokenIconButtonStandard.disabledIconColor, TokenIconButtonStandard.disabledIconOpacity)}; + } + if (toggleable && states.contains(WidgetState.selected)) { + return ${color(TokenIconButtonStandard.selectedIconColor)}; + } + return ${color(TokenIconButtonStandard.iconColor)}; + }); + + @override + WidgetStateProperty? get overlayColor => + WidgetStateProperty.resolveWith((Set states) { + if (toggleable && states.contains(WidgetState.selected)) { + if (states.contains(WidgetState.pressed)) { + return ${componentColor(TokenIconButtonStandard.selectedPressedStateLayerColor, TokenIconButtonStandard.pressedStateLayerOpacity)}; + } + if (states.contains(WidgetState.hovered)) { + return ${componentColor(TokenIconButtonStandard.selectedHoveredStateLayerColor, TokenIconButtonStandard.hoveredStateLayerOpacity)}; + } + if (states.contains(WidgetState.focused)) { + return ${componentColor(TokenIconButtonStandard.selectedFocusedStateLayerColor, TokenIconButtonStandard.focusedStateLayerOpacity)}; + } + } + if (states.contains(WidgetState.pressed)) { + return ${componentColor(TokenIconButtonStandard.pressedStateLayerColor, TokenIconButtonStandard.pressedStateLayerOpacity)}; + } + if (states.contains(WidgetState.hovered)) { + return ${componentColor(TokenIconButtonStandard.hoveredStateLayerColor, TokenIconButtonStandard.hoveredStateLayerOpacity)}; + } + if (states.contains(WidgetState.focused)) { + return ${componentColor(TokenIconButtonStandard.focusedStateLayerColor, TokenIconButtonStandard.focusedStateLayerOpacity)}; + } + return Colors.transparent; + }); + + @override + WidgetStateProperty? get elevation => + const MaterialStatePropertyAll(0.0); + + @override + WidgetStateProperty? get shadowColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get surfaceTintColor => + const MaterialStatePropertyAll(Colors.transparent); + +$_sizeDependentProperties + + @override + WidgetStateProperty? get side => null; + + @override + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; + + @override + VisualDensity? get visualDensity => VisualDensity.standard; + + @override + MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} +'''; + } + + String _generateFilledDefaults() { + return ''' +class _M3EFilledIconButtonDefaults extends ButtonStyle { + _M3EFilledIconButtonDefaults(this.context, this.toggleable, this.buttonSize, this.buttonWidth) + : super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; + final ButtonSize? buttonSize; + final IconButtonWidth? buttonWidth; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + WidgetStateProperty? get backgroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return ${componentColor(TokenIconButtonFilled.disabledContainerColor, TokenIconButtonFilled.disabledContainerOpacity)}; + } + if (toggleable && states.contains(WidgetState.selected)) { + return ${color(TokenIconButtonFilled.selectedContainerColor)}; + } + if (toggleable) { + return ${color(TokenIconButtonFilled.unselectedContainerColor)}; + } + return ${color(TokenIconButtonFilled.containerColor)}; + }); + + @override + WidgetStateProperty? get foregroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return ${componentColor(TokenIconButtonFilled.disabledIconColor, TokenIconButtonFilled.disabledIconOpacity)}; + } + if (toggleable && states.contains(WidgetState.selected)) { + return ${color(TokenIconButtonFilled.selectedIconColor)}; + } + if (toggleable) { + return ${color(TokenIconButtonFilled.unselectedIconColor)}; + } + return ${color(TokenIconButtonFilled.iconColor)}; + }); + + @override + WidgetStateProperty? get overlayColor => + WidgetStateProperty.resolveWith((Set states) { + if (toggleable && states.contains(WidgetState.selected)) { + if (states.contains(WidgetState.pressed)) { + return ${componentColor(TokenIconButtonFilled.selectedPressedStateLayerColor, TokenIconButtonFilled.pressedStateLayerOpacity)}; + } + if (states.contains(WidgetState.hovered)) { + return ${componentColor(TokenIconButtonFilled.selectedHoveredStateLayerColor, TokenIconButtonFilled.hoveredStateLayerOpacity)}; + } + if (states.contains(WidgetState.focused)) { + return ${componentColor(TokenIconButtonFilled.selectedFocusedStateLayerColor, TokenIconButtonFilled.focusedStateLayerOpacity)}; + } + } + if (toggleable) { + if (states.contains(WidgetState.pressed)) { + return ${componentColor(TokenIconButtonFilled.unselectedPressedStateLayerColor, TokenIconButtonFilled.pressedStateLayerOpacity)}; + } + if (states.contains(WidgetState.hovered)) { + return ${componentColor(TokenIconButtonFilled.unselectedHoveredStateLayerColor, TokenIconButtonFilled.hoveredStateLayerOpacity)}; + } + if (states.contains(WidgetState.focused)) { + return ${componentColor(TokenIconButtonFilled.unselectedFocusedStateLayerColor, TokenIconButtonFilled.focusedStateLayerOpacity)}; + } + } + if (states.contains(WidgetState.pressed)) { + return ${componentColor(TokenIconButtonFilled.pressedStateLayerColor, TokenIconButtonFilled.pressedStateLayerOpacity)}; + } + if (states.contains(WidgetState.hovered)) { + return ${componentColor(TokenIconButtonFilled.hoveredStateLayerColor, TokenIconButtonFilled.hoveredStateLayerOpacity)}; + } + if (states.contains(WidgetState.focused)) { + return ${componentColor(TokenIconButtonFilled.focusedStateLayerColor, TokenIconButtonFilled.focusedStateLayerOpacity)}; + } + return Colors.transparent; + }); + + @override + WidgetStateProperty? get elevation => + const MaterialStatePropertyAll(0.0); + + @override + WidgetStateProperty? get shadowColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get surfaceTintColor => + const MaterialStatePropertyAll(Colors.transparent); + +$_sizeDependentProperties + + @override + WidgetStateProperty? get side => null; + + @override + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; + + @override + VisualDensity? get visualDensity => VisualDensity.standard; + + @override + MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} +'''; + } + + String _generateFilledTonalDefaults() { + return ''' +class _M3EFilledTonalIconButtonDefaults extends ButtonStyle { + _M3EFilledTonalIconButtonDefaults(this.context, this.toggleable, this.buttonSize, this.buttonWidth) + : super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; + final ButtonSize? buttonSize; + final IconButtonWidth? buttonWidth; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + WidgetStateProperty? get backgroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return ${componentColor(TokenIconButtonTonal.disabledContainerColor, TokenIconButtonTonal.disabledContainerOpacity)}; + } + if (toggleable && states.contains(WidgetState.selected)) { + return ${color(TokenIconButtonTonal.selectedContainerColor)}; + } + if (toggleable) { + return ${color(TokenIconButtonTonal.unselectedContainerColor)}; + } + return ${color(TokenIconButtonTonal.containerColor)}; + }); + + @override + WidgetStateProperty? get foregroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return ${componentColor(TokenIconButtonTonal.disabledIconColor, TokenIconButtonTonal.disabledIconOpacity)}; + } + if (toggleable && states.contains(WidgetState.selected)) { + return ${color(TokenIconButtonTonal.selectedIconColor)}; + } + if (toggleable) { + return ${color(TokenIconButtonTonal.unselectedIconColor)}; + } + return ${color(TokenIconButtonTonal.iconColor)}; + }); + + @override + WidgetStateProperty? get overlayColor => + WidgetStateProperty.resolveWith((Set states) { + if (toggleable && states.contains(WidgetState.selected)) { + if (states.contains(WidgetState.pressed)) { + return ${componentColor(TokenIconButtonTonal.selectedPressedStateLayerColor, TokenIconButtonTonal.pressedStateLayerOpacity)}; + } + if (states.contains(WidgetState.hovered)) { + return ${componentColor(TokenIconButtonTonal.selectedHoveredStateLayerColor, TokenIconButtonTonal.hoveredStateLayerOpacity)}; + } + if (states.contains(WidgetState.focused)) { + return ${componentColor(TokenIconButtonTonal.selectedFocusedStateLayerColor, TokenIconButtonTonal.focusedStateLayerOpacity)}; + } + } + if (toggleable) { + if (states.contains(WidgetState.pressed)) { + return ${componentColor(TokenIconButtonTonal.unselectedPressedStateLayerColor, TokenIconButtonTonal.pressedStateLayerOpacity)}; + } + if (states.contains(WidgetState.hovered)) { + return ${componentColor(TokenIconButtonTonal.unselectedHoveredStateLayerColor, TokenIconButtonTonal.hoveredStateLayerOpacity)}; + } + if (states.contains(WidgetState.focused)) { + return ${componentColor(TokenIconButtonTonal.unselectedFocusedStateLayerColor, TokenIconButtonTonal.focusedStateLayerOpacity)}; + } + } + if (states.contains(WidgetState.pressed)) { + return ${componentColor(TokenIconButtonTonal.pressedStateLayerColor, TokenIconButtonTonal.pressedStateLayerOpacity)}; + } + if (states.contains(WidgetState.hovered)) { + return ${componentColor(TokenIconButtonTonal.hoveredStateLayerColor, TokenIconButtonTonal.hoveredStateLayerOpacity)}; + } + if (states.contains(WidgetState.focused)) { + return ${componentColor(TokenIconButtonTonal.focusedStateLayerColor, TokenIconButtonTonal.focusedStateLayerOpacity)}; + } + return Colors.transparent; + }); + + @override + WidgetStateProperty? get elevation => + const MaterialStatePropertyAll(0.0); + + @override + WidgetStateProperty? get shadowColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get surfaceTintColor => + const MaterialStatePropertyAll(Colors.transparent); + +$_sizeDependentProperties + + @override + WidgetStateProperty? get side => null; + + @override + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; + + @override + VisualDensity? get visualDensity => VisualDensity.standard; + + @override + MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} +'''; + } + + String _generateOutlinedDefaults() { + return ''' +class _M3EOutlinedIconButtonDefaults extends ButtonStyle { + _M3EOutlinedIconButtonDefaults(this.context, this.toggleable, this.buttonSize, this.buttonWidth) + : super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; + final ButtonSize? buttonSize; + final IconButtonWidth? buttonWidth; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + WidgetStateProperty? get backgroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + if (toggleable && states.contains(WidgetState.selected)) { + return ${componentColor(TokenIconButtonOutlined.selectedDisabledContainerColor, TokenIconButtonOutlined.selectedDisabledContainerOpacity)}; + } + return Colors.transparent; + } + if (toggleable && states.contains(WidgetState.selected)) { + return ${color(TokenIconButtonOutlined.selectedContainerColor)}; + } + return Colors.transparent; + }); + + @override + WidgetStateProperty? get foregroundColor => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return ${componentColor(TokenIconButtonOutlined.disabledIconColor, TokenIconButtonOutlined.disabledIconOpacity)}; + } + if (toggleable && states.contains(WidgetState.selected)) { + return ${color(TokenIconButtonOutlined.selectedIconColor)}; + } + return ${color(TokenIconButtonOutlined.iconColor)}; + }); + + @override + WidgetStateProperty? get overlayColor => + WidgetStateProperty.resolveWith((Set states) { + if (toggleable && states.contains(WidgetState.selected)) { + if (states.contains(WidgetState.pressed)) { + return ${componentColor(TokenIconButtonOutlined.selectedPressedStateLayerColor, TokenIconButtonOutlined.pressedStateLayerOpacity)}; + } + if (states.contains(WidgetState.hovered)) { + return ${componentColor(TokenIconButtonOutlined.selectedHoveredStateLayerColor, TokenIconButtonOutlined.hoveredStateLayerOpacity)}; + } + if (states.contains(WidgetState.focused)) { + return ${componentColor(TokenIconButtonOutlined.selectedFocusedStateLayerColor, TokenIconButtonOutlined.focusedStateLayerOpacity)}; + } + } + if (states.contains(WidgetState.pressed)) { + return ${componentColor(TokenIconButtonOutlined.pressedStateLayerColor, TokenIconButtonOutlined.pressedStateLayerOpacity)}; + } + if (states.contains(WidgetState.hovered)) { + return ${componentColor(TokenIconButtonOutlined.hoveredStateLayerColor, TokenIconButtonOutlined.hoveredStateLayerOpacity)}; + } + if (states.contains(WidgetState.focused)) { + return ${componentColor(TokenIconButtonOutlined.focusedStateLayerColor, TokenIconButtonOutlined.focusedStateLayerOpacity)}; + } + return Colors.transparent; + }); + + @override + WidgetStateProperty? get elevation => + const MaterialStatePropertyAll(0.0); + + @override + WidgetStateProperty? get shadowColor => + const MaterialStatePropertyAll(Colors.transparent); + + @override + WidgetStateProperty? get surfaceTintColor => + const MaterialStatePropertyAll(Colors.transparent); + +$_sizeDependentProperties + + @override + WidgetStateProperty? get side => + WidgetStateProperty.resolveWith((Set states) { + if (toggleable && states.contains(WidgetState.selected)) { + return null; + } + if (states.contains(WidgetState.disabled)) { + return BorderSide(color: ${color(TokenIconButtonOutlined.unselectedDisabledOutlineColor)}, width: $_outlineWidthSwitch); + } + return BorderSide(color: ${color(TokenIconButtonOutlined.outlineColor)}, width: $_outlineWidthSwitch); + }); + + @override + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; + + @override + VisualDensity? get visualDensity => VisualDensity.standard; + + @override + MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} +'''; + } +} diff --git a/packages/material_ui/tool/gen_defaults/templates/template.dart b/packages/material_ui/tool/gen_defaults/templates/template.dart index d619a217766f..41d58104549a 100644 --- a/packages/material_ui/tool/gen_defaults/templates/template.dart +++ b/packages/material_ui/tool/gen_defaults/templates/template.dart @@ -6,6 +6,9 @@ import 'dart:io'; import 'package:meta/meta.dart'; +import '../data/color_role.dart'; +import '../data/shape_struct.dart'; + enum _MaterialVersion { material3, material3Expressive } /// A template for generating Material 3 component defaults. @@ -93,6 +96,51 @@ abstract class TokenTemplate { /// The [className] parameter must be used to declare the class. String generateContents(String className); + String color(TokenColorRole role) { + return '_colors.${_colorSchemeName(role)}'; + } + + String componentColor(TokenColorRole role, [double? opacity]) { + String value = color(role); + if (opacity != null && opacity != 1.0) { + value += '.withOpacity($opacity)'; + } + return value; + } + + String _colorSchemeName(TokenColorRole role) { + return switch (role) { + TokenColorRole.inverseOnSurface => 'onInverseSurface', + _ => role.name, + }; + } + + String shape(ShapeStruct token) { + final isCircular = token.family == 'SHAPE_FAMILY_CIRCULAR'; + final bool hasUniformCorners = + token.topLeft == token.topRight && + token.topLeft == token.bottomLeft && + token.topLeft == token.bottomRight; + + if (isCircular) { + return 'const StadiumBorder()'; + } + + if (hasUniformCorners) { + return 'const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(${token.topLeft})))'; + } + + return ''' +const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(${token.topLeft}), + topRight: Radius.circular(${token.topRight}), + bottomLeft: Radius.circular(${token.bottomLeft}), + bottomRight: Radius.circular(${token.bottomRight}), + ), +)'''; + } + /// Generates the file under the target path [materialLib] and formats it. void generateFile({bool verbose = false}) { final String snakeName = name.toLowerCase().replaceAll(' ', '_');