diff --git a/packages/material_ui/lib/material_3.dart b/packages/material_ui/lib/material_3.dart new file mode 100644 index 000000000000..19dba6ffe856 --- /dev/null +++ b/packages/material_ui/lib/material_3.dart @@ -0,0 +1,8 @@ +// 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. + +/// Flutter widgets implementing Material Design 3. +library material_3; + +export 'material_ui.dart'; diff --git a/packages/material_ui/lib/material_3_expressive.dart b/packages/material_ui/lib/material_3_expressive.dart new file mode 100644 index 000000000000..cee0f1e0db3f --- /dev/null +++ b/packages/material_ui/lib/material_3_expressive.dart @@ -0,0 +1,9 @@ +// 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. + +/// Flutter widgets implementing Material 3 Expressive. +library material_3_expressive; + +export 'material_ui.dart' hide IconButton; +export 'src/material_3_expressive/icon_button.dart'; diff --git a/packages/material_ui/lib/src/button_style.dart b/packages/material_ui/lib/src/button_style.dart index 74ebec4e6eb0..8409323a94e3 100644 --- a/packages/material_ui/lib/src/button_style.dart +++ b/packages/material_ui/lib/src/button_style.dart @@ -1,4 +1,4 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. +// 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. @@ -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/button_style_button.dart b/packages/material_ui/lib/src/button_style_button.dart index 71be80bcc85c..e11c668666cf 100644 --- a/packages/material_ui/lib/src/button_style_button.dart +++ b/packages/material_ui/lib/src/button_style_button.dart @@ -1,4 +1,4 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. +// 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. @@ -87,6 +87,7 @@ abstract class ButtonStyleButton extends StatefulWidget { required this.clipBehavior, this.statesController, this.isSemanticButton = true, + this.isSelected, @Deprecated( 'Remove this parameter as it is now ignored. ' 'Use ButtonStyle.iconAlignment instead. ' @@ -163,6 +164,9 @@ abstract class ButtonStyleButton extends StatefulWidget { /// Defaults to true. final bool? isSemanticButton; + /// Whether the button is selected. + final bool? isSelected; + /// {@macro flutter.material.ButtonStyle.iconAlignment} @Deprecated( 'Remove this parameter as it is now ignored. ' @@ -334,6 +338,9 @@ class _ButtonStyleState extends State with TickerProviderStat internalStatesController = MaterialStatesController(); } statesController.update(WidgetState.disabled, !widget.enabled); + if (widget.isSelected != null) { + statesController.update(WidgetState.selected, widget.isSelected!); + } statesController.addListener(handleStatesControllerChange); } @@ -361,6 +368,9 @@ class _ButtonStyleState extends State with TickerProviderStat statesController.update(WidgetState.pressed, false); } } + if (widget.isSelected != oldWidget.isSelected) { + statesController.update(WidgetState.selected, widget.isSelected ?? false); + } } @override @@ -592,6 +602,7 @@ class _ButtonStyleState extends State with TickerProviderStat container: true, button: widget.isSemanticButton, enabled: widget.enabled, + selected: widget.isSelected, child: _InputPadding( minSize: minSize, child: ConstrainedBox( diff --git a/packages/material_ui/lib/src/generated/material_3_expressive/icon_button_defaults.g.dart b/packages/material_ui/lib/src/generated/material_3_expressive/icon_button_defaults.g.dart new file mode 100644 index 000000000000..7c644227ac70 --- /dev/null +++ b/packages/material_ui/lib/src/generated/material_3_expressive/icon_button_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 '../../material_3_expressive/icon_button.dart'; + +class _IconButtonDefaultsM3E extends ButtonStyle { + _IconButtonDefaultsM3E(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 _FilledIconButtonDefaultsM3E extends ButtonStyle { + _FilledIconButtonDefaultsM3E(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 _FilledTonalIconButtonDefaultsM3E extends ButtonStyle { + _FilledTonalIconButtonDefaultsM3E( + 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 _OutlinedIconButtonDefaultsM3E extends ButtonStyle { + _OutlinedIconButtonDefaultsM3E(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/material_3_expressive/icon_button.dart b/packages/material_ui/lib/src/material_3_expressive/icon_button.dart new file mode 100644 index 000000000000..0e53692c1f6d --- /dev/null +++ b/packages/material_ui/lib/src/material_3_expressive/icon_button.dart @@ -0,0 +1,288 @@ +// 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. + +library; + +import 'package:flutter/widgets.dart'; + +import '../button_style.dart'; +import '../button_style_button.dart'; +import '../color_scheme.dart'; +import '../colors.dart'; +import '../constants.dart'; +import '../icon_button_theme.dart'; +import '../ink_well.dart'; +import '../material_state.dart'; +import '../theme.dart'; +import '../theme_data.dart'; + +part '../generated/material_3_expressive/icon_button_defaults.g.dart'; + +enum _IconButtonVariant { standard, filled, filledTonal, outlined } + +/// A Material Design 3 Expressive icon button. +/// +/// M3 Expressive icon buttons support five size variants ([ButtonSize]), +/// shape morphing on press and selection, and updated color tokens. +/// +/// Use [IconButton] for a standard icon button, [IconButton.filled] for a +/// filled icon button, [IconButton.filledTonal] for a filled tonal icon button, +/// and [IconButton.outlined] for an outlined icon button. +/// +/// The button dimensions are controlled by [ButtonStyle.size]. If not +/// provided, the effective size defaults to [ButtonSize.small] (40dp), or to +/// the size specified by [IconButtonThemeData.style]. +/// +/// {@tool dartpad} +/// This sample shows how to use M3E [IconButton] with different sizes. +/// +/// ** See code in examples/api/lib/material/icon_button/icon_button.0.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * +class IconButton extends ButtonStyleButton { + /// Creates a Material Design 3 Expressive icon button. + const IconButton({ + super.key, + super.style, + super.focusNode, + super.autofocus = false, + super.tooltip, + super.onPressed, + super.onHover, + super.onLongPress, + super.isSelected, + this.selectedIcon, + super.statesController, + required this.icon, + }) : _variant = _IconButtonVariant.standard, + super( + onFocusChange: null, + clipBehavior: Clip.none, + child: (isSelected ?? false) ? selectedIcon ?? icon : icon, + ); + + /// Creates a filled Material Design 3 Expressive icon button. + const IconButton.filled({ + super.key, + super.style, + super.focusNode, + super.autofocus = false, + super.tooltip, + super.onPressed, + super.onHover, + super.onLongPress, + super.isSelected, + this.selectedIcon, + super.statesController, + required this.icon, + }) : _variant = _IconButtonVariant.filled, + super( + onFocusChange: null, + clipBehavior: Clip.none, + child: (isSelected ?? false) ? selectedIcon ?? icon : icon, + ); + + /// Creates a filled tonal Material Design 3 Expressive icon button. + const IconButton.filledTonal({ + super.key, + super.style, + super.focusNode, + super.autofocus = false, + super.tooltip, + super.onPressed, + super.onHover, + super.onLongPress, + super.isSelected, + this.selectedIcon, + super.statesController, + required this.icon, + }) : _variant = _IconButtonVariant.filledTonal, + super( + onFocusChange: null, + clipBehavior: Clip.none, + child: (isSelected ?? false) ? selectedIcon ?? icon : icon, + ); + + /// Creates an outlined Material Design 3 Expressive icon button. + const IconButton.outlined({ + super.key, + super.style, + super.focusNode, + super.autofocus = false, + super.tooltip, + super.onPressed, + super.onHover, + super.onLongPress, + super.isSelected, + this.selectedIcon, + super.statesController, + required this.icon, + }) : _variant = _IconButtonVariant.outlined, + super( + onFocusChange: null, + clipBehavior: Clip.none, + child: (isSelected ?? false) ? selectedIcon ?? icon : icon, + ); + + /// The icon to display inside the button. + /// + /// The [Icon.size] and [Icon.color] of the icon are configured automatically + /// from the resolved [ButtonStyle] using an [IconTheme] and therefore should + /// not be explicitly given in the icon widget. + /// + /// See [Icon], [ImageIcon]. + final Widget icon; + + /// The icon to display inside the button when [isSelected] is true. + /// + /// If this is null, [icon] is used for both selected and unselected states. + final Widget? selectedIcon; + + final _IconButtonVariant _variant; + + /// A static convenience method that constructs an icon button [ButtonStyle] + /// given simple values. + static ButtonStyle styleFrom({ + Color? foregroundColor, + Color? backgroundColor, + Color? disabledForegroundColor, + Color? disabledBackgroundColor, + Color? focusColor, + Color? hoverColor, + Color? highlightColor, + Color? shadowColor, + Color? surfaceTintColor, + Color? overlayColor, + double? elevation, + Size? minimumSize, + Size? fixedSize, + Size? maximumSize, + double? iconSize, + BorderSide? side, + OutlinedBorder? shape, + EdgeInsetsGeometry? padding, + MouseCursor? enabledMouseCursor, + MouseCursor? disabledMouseCursor, + VisualDensity? visualDensity, + MaterialTapTargetSize? tapTargetSize, + Duration? animationDuration, + bool? enableFeedback, + AlignmentGeometry? alignment, + InteractiveInkFeatureFactory? splashFactory, + ButtonSize? size, + IconButtonWidth? iconButtonWidth, + }) { + final Color? overlayFallback = overlayColor ?? foregroundColor; + WidgetStateProperty? overlayColorProp; + if ((hoverColor ?? focusColor ?? highlightColor ?? overlayFallback) != null) { + overlayColorProp = switch (overlayColor) { + Color(a: 0.0) => WidgetStatePropertyAll(overlayColor), + _ => WidgetStateProperty.fromMap({ + WidgetState.pressed: highlightColor ?? overlayFallback?.withOpacity(0.1), + WidgetState.hovered: hoverColor ?? overlayFallback?.withOpacity(0.08), + WidgetState.focused: focusColor ?? overlayFallback?.withOpacity(0.1), + }), + }; + } + + return ButtonStyle( + backgroundColor: ButtonStyleButton.defaultColor(backgroundColor, disabledBackgroundColor), + foregroundColor: ButtonStyleButton.defaultColor(foregroundColor, disabledForegroundColor), + overlayColor: overlayColorProp, + shadowColor: ButtonStyleButton.allOrNull(shadowColor), + surfaceTintColor: ButtonStyleButton.allOrNull(surfaceTintColor), + elevation: ButtonStyleButton.allOrNull(elevation), + padding: ButtonStyleButton.allOrNull(padding), + minimumSize: ButtonStyleButton.allOrNull(minimumSize), + fixedSize: ButtonStyleButton.allOrNull(fixedSize), + maximumSize: ButtonStyleButton.allOrNull(maximumSize), + iconSize: ButtonStyleButton.allOrNull(iconSize), + side: ButtonStyleButton.allOrNull(side), + shape: ButtonStyleButton.allOrNull(shape), + mouseCursor: disabledMouseCursor == null && enabledMouseCursor == null + ? null + : WidgetStateProperty.fromMap({ + WidgetState.disabled: disabledMouseCursor, + WidgetState.any: enabledMouseCursor, + }), + visualDensity: visualDensity, + tapTargetSize: tapTargetSize, + animationDuration: animationDuration, + enableFeedback: enableFeedback, + alignment: alignment, + splashFactory: splashFactory, + size: size, + iconButtonWidth: iconButtonWidth, + ); + } + + /// Resolves the effective [ButtonSize] from widget, theme, and defaults. + ButtonSize _resolveSize(BuildContext context) { + return style?.size ?? IconButtonTheme.of(context).style?.size ?? ButtonSize.small; + } + + /// Resolves the effective [IconButtonWidth] from widget, theme, and defaults. + IconButtonWidth _resolveWidth(BuildContext context) { + return style?.iconButtonWidth ?? + IconButtonTheme.of(context).style?.iconButtonWidth ?? + IconButtonWidth.standard; + } + + @override + ButtonStyle defaultStyleOf(BuildContext context) { + final ButtonSize effectiveSize = _resolveSize(context); + final IconButtonWidth effectiveWidth = _resolveWidth(context); + final ButtonStyle style = switch (_variant) { + _IconButtonVariant.filled => _FilledIconButtonDefaultsM3E( + context, + isSelected != null, + effectiveSize, + effectiveWidth, + ), + _IconButtonVariant.filledTonal => _FilledTonalIconButtonDefaultsM3E( + context, + isSelected != null, + effectiveSize, + effectiveWidth, + ), + _IconButtonVariant.outlined => _OutlinedIconButtonDefaultsM3E( + context, + isSelected != null, + effectiveSize, + effectiveWidth, + ), + _IconButtonVariant.standard => _IconButtonDefaultsM3E( + context, + isSelected != null, + effectiveSize, + effectiveWidth, + ), + }; + return style; + } + + /// Returns the [IconButtonThemeData.style] of the closest [IconButtonTheme] ancestor. + /// The color and icon size can also be configured by the [IconTheme] if the same property + /// has a null value in [IconButtonTheme]. However, if any of the properties exist + /// in both [IconButtonTheme] and [IconTheme], [IconTheme] will be overridden. + @override + ButtonStyle? themeStyleOf(BuildContext context) { + final IconThemeData iconTheme = IconTheme.of(context); + final isDefaultSize = iconTheme.size == const IconThemeData.fallback().size; + final bool isDefaultColor = identical(iconTheme.color, switch (Theme.brightnessOf(context)) { + Brightness.light => kDefaultIconDarkColor, + Brightness.dark => kDefaultIconLightColor, + }); + + final ButtonStyle iconThemeStyle = IconButton.styleFrom( + foregroundColor: isDefaultColor ? null : iconTheme.color, + iconSize: isDefaultSize ? null : iconTheme.size, + ); + + return IconButtonTheme.of(context).style?.merge(iconThemeStyle) ?? iconThemeStyle; + } +} diff --git a/packages/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..19e7b9eae980 --- /dev/null +++ b/packages/material_ui/test/material_3_expressive_icon_button_test.dart @@ -0,0 +1,645 @@ +// 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_3_expressive.dart'; + +void main() { + // Helper to create a testable icon button. + Widget buildApp({required Widget child, ThemeData? theme}) { + return MaterialApp( + theme: theme ?? ThemeData(useMaterial3: true), + 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), + ), + ), + 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)), + ), + 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)), + ), + 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)), + 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(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 enabled', (WidgetTester tester) async { + var longPressed = false; + final SemanticsHandle handle = tester.ensureSemantics(); + + await tester.pumpWidget( + buildApp( + child: IconButton( + onLongPress: () { + longPressed = true; + }, + icon: const Icon(Icons.add, semanticLabel: 'add'), + ), + ), + ); + + expect( + tester.getSemantics(find.byType(IconButton)), + matchesSemantics( + hasLongPressAction: true, + hasFocusAction: true, + hasEnabledState: true, + isButton: true, + isEnabled: true, + isFocusable: true, + label: 'add', + ), + ); + + await tester.longPress(find.byType(IconButton)); + expect(longPressed, isTrue); + handle.dispose(); + }); + + testWidgets('disabled filled button has reduced background', (WidgetTester tester) async { + await tester.pumpWidget(buildApp(child: const IconButton.filled(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 barrel file import', () { + testWidgets('material_3_expressive.dart import provides M3E IconButton', ( + 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/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..39a2e1127788 --- /dev/null +++ b/packages/material_ui/tool/gen_defaults/templates/icon_button_template.dart @@ -0,0 +1,694 @@ +// 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 generateContents() { + return ''' +${_generateStandardDefaults()} +${_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() { + return ''' +class _IconButtonDefaultsM3E extends ButtonStyle { + _IconButtonDefaultsM3E(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 _FilledIconButtonDefaultsM3E extends ButtonStyle { + _FilledIconButtonDefaultsM3E(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 _FilledTonalIconButtonDefaultsM3E extends ButtonStyle { + _FilledTonalIconButtonDefaultsM3E(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 _OutlinedIconButtonDefaultsM3E extends ButtonStyle { + _OutlinedIconButtonDefaultsM3E(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 e10ffca26693..b8956fa5d0bc 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 } abstract class M3TokenTemplate extends _TokenTemplate { @@ -61,6 +64,51 @@ abstract class _TokenTemplate { String generateContents(); + 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}), + ), +)'''; + } + void generateFile({bool verbose = false}) { final fileName = '$materialLib/${name}_defaults.g.dart'; if (verbose) {