diff --git a/packages/material_ui/lib/src/button_style.dart b/packages/material_ui/lib/src/button_style.dart index e251296f42ed..e12120be7555 100644 --- a/packages/material_ui/lib/src/button_style.dart +++ b/packages/material_ui/lib/src/button_style.dart @@ -31,6 +31,55 @@ import 'theme_data.dart'; // late BuildContext context; // typedef MyAppHome = Placeholder; +/// Defines size variants for Material 3 Expressive button components. +/// +/// Each size variant represents a named visual scale. For example, a Material +/// 3 Expressive [IconButton] uses this value to choose its container size, icon +/// size, and padding. +enum ButtonSizeVariant { + /// The smallest button size. + xSmall, + + /// A small button size. + small, + + /// A medium button size. + medium, + + /// A large button size. + large, + + /// The largest button size. + xLarge, +} + +/// Defines the width variants for Material 3 Expressive [IconButton]. +enum IconButtonWidthVariant { + /// A narrower icon button. + narrow, + + /// The standard icon button width. + standard, + + /// A wider icon button. + wide, +} + +/// Defines shape variants for Material 3 Expressive button components. +enum ButtonShapeVariant { + /// A rounded button shape. + /// + /// For example, a Material 3 Expressive [IconButton] with this variant uses + /// rounder default container shapes. + round, + + /// A squared button shape. + /// + /// For example, a Material 3 Expressive [IconButton] with this variant uses + /// more squared default container shapes. + square, +} + /// 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 +236,9 @@ class ButtonStyle with Diagnosticable { this.splashFactory, this.backgroundBuilder, this.foregroundBuilder, + this.sizeVariant, + this.iconButtonWidth, + this.shapeVariant, }); /// The style for a button's [Text] widget descendants. @@ -423,6 +475,42 @@ class ButtonStyle with Diagnosticable { /// configuring clipping. final ButtonLayerBuilder? foregroundBuilder; + /// The Material 3 Expressive size variant for this button. + /// + /// A size variant selects a component-defined token set rather than a single + /// dimension. For example, an [IconButton] uses this value to choose its + /// default container size, padding, and icon size when it is using + /// [StyleVariant.material3Expressive]. + /// + /// Use [minimumSize], [fixedSize], [maximumSize], [padding], or [iconSize] to + /// directly override those individual style properties. + final ButtonSizeVariant? sizeVariant; + + /// The Material 3 Expressive width variant for an [IconButton]. + /// + /// A width variant selects the leading and trailing space tokens used to + /// compute an icon button's default horizontal padding and minimum width when + /// it is using [StyleVariant.material3Expressive]. It does not change the + /// button's height or icon size. + /// + /// This property is only used by icon buttons. Use [padding], + /// [minimumSize], [fixedSize], or [maximumSize] to directly override the + /// resulting layout. + final IconButtonWidthVariant? iconButtonWidth; + + /// The Material 3 Expressive shape variant for this button. + /// + /// A shape variant selects between component-defined token families, such as + /// round and square shapes. For [IconButton], this affects the default + /// container and selected-state shapes when it is using + /// [StyleVariant.material3Expressive]; state-specific tokens, such as the + /// pressed shape, are still resolved by the component defaults. + /// + /// Use [shape] to provide a specific [OutlinedBorder] or stateful shape + /// override instead of selecting one of the Material 3 Expressive shape + /// variants. + final ButtonShapeVariant? shapeVariant; + /// Returns a copy of this ButtonStyle with the given fields replaced with /// the new values. ButtonStyle copyWith({ @@ -451,6 +539,9 @@ class ButtonStyle with Diagnosticable { InteractiveInkFeatureFactory? splashFactory, ButtonLayerBuilder? backgroundBuilder, ButtonLayerBuilder? foregroundBuilder, + ButtonSizeVariant? sizeVariant, + IconButtonWidthVariant? iconButtonWidth, + ButtonShapeVariant? shapeVariant, }) { return ButtonStyle( textStyle: textStyle ?? this.textStyle, @@ -478,6 +569,9 @@ class ButtonStyle with Diagnosticable { splashFactory: splashFactory ?? this.splashFactory, backgroundBuilder: backgroundBuilder ?? this.backgroundBuilder, foregroundBuilder: foregroundBuilder ?? this.foregroundBuilder, + sizeVariant: sizeVariant ?? this.sizeVariant, + iconButtonWidth: iconButtonWidth ?? this.iconButtonWidth, + shapeVariant: shapeVariant ?? this.shapeVariant, ); } @@ -516,6 +610,9 @@ class ButtonStyle with Diagnosticable { splashFactory: splashFactory ?? style.splashFactory, backgroundBuilder: backgroundBuilder ?? style.backgroundBuilder, foregroundBuilder: foregroundBuilder ?? style.foregroundBuilder, + sizeVariant: sizeVariant ?? style.sizeVariant, + iconButtonWidth: iconButtonWidth ?? style.iconButtonWidth, + shapeVariant: shapeVariant ?? style.shapeVariant, ); } @@ -547,6 +644,9 @@ class ButtonStyle with Diagnosticable { splashFactory, backgroundBuilder, foregroundBuilder, + sizeVariant, + iconButtonWidth, + shapeVariant, ]; return Object.hashAll(values); } @@ -584,7 +684,10 @@ class ButtonStyle with Diagnosticable { other.alignment == alignment && other.splashFactory == splashFactory && other.backgroundBuilder == backgroundBuilder && - other.foregroundBuilder == foregroundBuilder; + other.foregroundBuilder == foregroundBuilder && + other.sizeVariant == sizeVariant && + other.iconButtonWidth == iconButtonWidth && + other.shapeVariant == shapeVariant; } @override @@ -706,6 +809,13 @@ class ButtonStyle with Diagnosticable { defaultValue: null, ), ); + properties.add(EnumProperty('sizeVariant', sizeVariant, defaultValue: null)); + properties.add( + EnumProperty('iconButtonWidth', iconButtonWidth, defaultValue: null), + ); + properties.add( + EnumProperty('shapeVariant', shapeVariant, defaultValue: null), + ); } /// Linearly interpolate between two [ButtonStyle]s. @@ -769,6 +879,9 @@ 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, + sizeVariant: t < 0.5 ? a?.sizeVariant : b?.sizeVariant, + iconButtonWidth: t < 0.5 ? a?.iconButtonWidth : b?.iconButtonWidth, + shapeVariant: t < 0.5 ? a?.shapeVariant : b?.shapeVariant, ); } } diff --git a/packages/material_ui/lib/src/generated/icon_button_defaults_m3e.g.dart b/packages/material_ui/lib/src/generated/icon_button_defaults_m3e.g.dart new file mode 100644 index 000000000000..4c3c68d1b040 --- /dev/null +++ b/packages/material_ui/lib/src/generated/icon_button_defaults_m3e.g.dart @@ -0,0 +1,1168 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Do not edit by hand. The code is generated from data in the Material +// Design token database by the script: +// packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart. +part of '../icon_button.dart'; + +class _IconButtonDefaultsM3E extends ButtonStyle { + _IconButtonDefaultsM3E( + this.context, + this.toggleable, + ButtonSizeVariant? sizeVariant, + IconButtonWidthVariant? iconButtonWidth, + ButtonShapeVariant? shapeVariant, + ) : _sizeVariant = sizeVariant, + _iconButtonWidth = iconButtonWidth, + _shapeVariant = shapeVariant, + super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; + final ButtonSizeVariant? _sizeVariant; + final IconButtonWidthVariant? _iconButtonWidth; + final ButtonShapeVariant? _shapeVariant; + + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + ButtonSizeVariant get sizeVariant => _sizeVariant ?? ButtonSizeVariant.small; + + @override + IconButtonWidthVariant get iconButtonWidth => _iconButtonWidth ?? IconButtonWidthVariant.standard; + + @override + ButtonShapeVariant get shapeVariant => _shapeVariant ?? ButtonShapeVariant.round; + + @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 (sizeVariant) { + ButtonSizeVariant.xSmall => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 6.0, 4.0, 6.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB(6.0, 6.0, 6.0, 6.0), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(10.0, 6.0, 10.0, 6.0), + }, + ButtonSizeVariant.small => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 8.0, 4.0, 8.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB(8.0, 8.0, 8.0, 8.0), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(14.0, 8.0, 14.0, 8.0), + }, + ButtonSizeVariant.medium => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(12.0, 16.0, 12.0, 16.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB( + 16.0, + 16.0, + 16.0, + 16.0, + ), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(24.0, 16.0, 24.0, 16.0), + }, + ButtonSizeVariant.large => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(16.0, 32.0, 16.0, 32.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB( + 32.0, + 32.0, + 32.0, + 32.0, + ), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(48.0, 32.0, 48.0, 32.0), + }, + ButtonSizeVariant.xLarge => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(32.0, 48.0, 32.0, 48.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB( + 48.0, + 48.0, + 48.0, + 48.0, + ), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(72.0, 48.0, 72.0, 48.0), + }, + }); + + @override + WidgetStateProperty? get minimumSize => + MaterialStatePropertyAll(switch (sizeVariant) { + ButtonSizeVariant.xSmall => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(28.0, 32.0), + IconButtonWidthVariant.standard => const Size(32.0, 32.0), + IconButtonWidthVariant.wide => const Size(40.0, 32.0), + }, + ButtonSizeVariant.small => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(32.0, 40.0), + IconButtonWidthVariant.standard => const Size(40.0, 40.0), + IconButtonWidthVariant.wide => const Size(52.0, 40.0), + }, + ButtonSizeVariant.medium => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(48.0, 56.0), + IconButtonWidthVariant.standard => const Size(56.0, 56.0), + IconButtonWidthVariant.wide => const Size(72.0, 56.0), + }, + ButtonSizeVariant.large => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(64.0, 96.0), + IconButtonWidthVariant.standard => const Size(96.0, 96.0), + IconButtonWidthVariant.wide => const Size(128.0, 96.0), + }, + ButtonSizeVariant.xLarge => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(104.0, 136.0), + IconButtonWidthVariant.standard => const Size(136.0, 136.0), + IconButtonWidthVariant.wide => const Size(184.0, 136.0), + }, + }); + + @override + WidgetStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); + + @override + WidgetStateProperty? get iconSize => + MaterialStatePropertyAll(switch (sizeVariant) { + ButtonSizeVariant.xSmall => 20.0, + ButtonSizeVariant.small => 24.0, + ButtonSizeVariant.medium => 24.0, + ButtonSizeVariant.large => 32.0, + ButtonSizeVariant.xLarge => 40.0, + }); + + @override + WidgetStateProperty? get shape => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.pressed)) { + return switch (sizeVariant) { + ButtonSizeVariant.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSizeVariant.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSizeVariant.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSizeVariant.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + }; + } + if (toggleable && states.contains(WidgetState.selected)) { + return switch (shapeVariant) { + ButtonShapeVariant.round => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSizeVariant.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + ButtonSizeVariant.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + }, + ButtonShapeVariant.square => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const StadiumBorder(), + ButtonSizeVariant.small => const StadiumBorder(), + ButtonSizeVariant.medium => const StadiumBorder(), + ButtonSizeVariant.large => const StadiumBorder(), + ButtonSizeVariant.xLarge => const StadiumBorder(), + }, + }; + } + return switch (shapeVariant) { + ButtonShapeVariant.round => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const StadiumBorder(), + ButtonSizeVariant.small => const StadiumBorder(), + ButtonSizeVariant.medium => const StadiumBorder(), + ButtonSizeVariant.large => const StadiumBorder(), + ButtonSizeVariant.xLarge => const StadiumBorder(), + }, + ButtonShapeVariant.square => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSizeVariant.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + ButtonSizeVariant.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + }, + }; + }); + + @override + WidgetStateProperty? get side => null; + + @override + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; + + @override + VisualDensity? get visualDensity => VisualDensity.standard; + + @override + MaterialTapTargetSize? get tapTargetSize => MaterialTapTargetSize.padded; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} + +class _FilledIconButtonDefaultsM3E extends ButtonStyle { + _FilledIconButtonDefaultsM3E( + this.context, + this.toggleable, + ButtonSizeVariant? sizeVariant, + IconButtonWidthVariant? iconButtonWidth, + ButtonShapeVariant? shapeVariant, + ) : _sizeVariant = sizeVariant, + _iconButtonWidth = iconButtonWidth, + _shapeVariant = shapeVariant, + super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; + final ButtonSizeVariant? _sizeVariant; + final IconButtonWidthVariant? _iconButtonWidth; + final ButtonShapeVariant? _shapeVariant; + + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + ButtonSizeVariant get sizeVariant => _sizeVariant ?? ButtonSizeVariant.small; + + @override + IconButtonWidthVariant get iconButtonWidth => _iconButtonWidth ?? IconButtonWidthVariant.standard; + + @override + ButtonShapeVariant get shapeVariant => _shapeVariant ?? ButtonShapeVariant.round; + + @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 (sizeVariant) { + ButtonSizeVariant.xSmall => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 6.0, 4.0, 6.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB(6.0, 6.0, 6.0, 6.0), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(10.0, 6.0, 10.0, 6.0), + }, + ButtonSizeVariant.small => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 8.0, 4.0, 8.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB(8.0, 8.0, 8.0, 8.0), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(14.0, 8.0, 14.0, 8.0), + }, + ButtonSizeVariant.medium => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(12.0, 16.0, 12.0, 16.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB( + 16.0, + 16.0, + 16.0, + 16.0, + ), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(24.0, 16.0, 24.0, 16.0), + }, + ButtonSizeVariant.large => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(16.0, 32.0, 16.0, 32.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB( + 32.0, + 32.0, + 32.0, + 32.0, + ), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(48.0, 32.0, 48.0, 32.0), + }, + ButtonSizeVariant.xLarge => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(32.0, 48.0, 32.0, 48.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB( + 48.0, + 48.0, + 48.0, + 48.0, + ), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(72.0, 48.0, 72.0, 48.0), + }, + }); + + @override + WidgetStateProperty? get minimumSize => + MaterialStatePropertyAll(switch (sizeVariant) { + ButtonSizeVariant.xSmall => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(28.0, 32.0), + IconButtonWidthVariant.standard => const Size(32.0, 32.0), + IconButtonWidthVariant.wide => const Size(40.0, 32.0), + }, + ButtonSizeVariant.small => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(32.0, 40.0), + IconButtonWidthVariant.standard => const Size(40.0, 40.0), + IconButtonWidthVariant.wide => const Size(52.0, 40.0), + }, + ButtonSizeVariant.medium => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(48.0, 56.0), + IconButtonWidthVariant.standard => const Size(56.0, 56.0), + IconButtonWidthVariant.wide => const Size(72.0, 56.0), + }, + ButtonSizeVariant.large => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(64.0, 96.0), + IconButtonWidthVariant.standard => const Size(96.0, 96.0), + IconButtonWidthVariant.wide => const Size(128.0, 96.0), + }, + ButtonSizeVariant.xLarge => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(104.0, 136.0), + IconButtonWidthVariant.standard => const Size(136.0, 136.0), + IconButtonWidthVariant.wide => const Size(184.0, 136.0), + }, + }); + + @override + WidgetStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); + + @override + WidgetStateProperty? get iconSize => + MaterialStatePropertyAll(switch (sizeVariant) { + ButtonSizeVariant.xSmall => 20.0, + ButtonSizeVariant.small => 24.0, + ButtonSizeVariant.medium => 24.0, + ButtonSizeVariant.large => 32.0, + ButtonSizeVariant.xLarge => 40.0, + }); + + @override + WidgetStateProperty? get shape => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.pressed)) { + return switch (sizeVariant) { + ButtonSizeVariant.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSizeVariant.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSizeVariant.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSizeVariant.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + }; + } + if (toggleable && states.contains(WidgetState.selected)) { + return switch (shapeVariant) { + ButtonShapeVariant.round => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSizeVariant.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + ButtonSizeVariant.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + }, + ButtonShapeVariant.square => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const StadiumBorder(), + ButtonSizeVariant.small => const StadiumBorder(), + ButtonSizeVariant.medium => const StadiumBorder(), + ButtonSizeVariant.large => const StadiumBorder(), + ButtonSizeVariant.xLarge => const StadiumBorder(), + }, + }; + } + return switch (shapeVariant) { + ButtonShapeVariant.round => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const StadiumBorder(), + ButtonSizeVariant.small => const StadiumBorder(), + ButtonSizeVariant.medium => const StadiumBorder(), + ButtonSizeVariant.large => const StadiumBorder(), + ButtonSizeVariant.xLarge => const StadiumBorder(), + }, + ButtonShapeVariant.square => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSizeVariant.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + ButtonSizeVariant.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + }, + }; + }); + + @override + WidgetStateProperty? get side => null; + + @override + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; + + @override + VisualDensity? get visualDensity => VisualDensity.standard; + + @override + MaterialTapTargetSize? get tapTargetSize => MaterialTapTargetSize.padded; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} + +class _FilledTonalIconButtonDefaultsM3E extends ButtonStyle { + _FilledTonalIconButtonDefaultsM3E( + this.context, + this.toggleable, + ButtonSizeVariant? sizeVariant, + IconButtonWidthVariant? iconButtonWidth, + ButtonShapeVariant? shapeVariant, + ) : _sizeVariant = sizeVariant, + _iconButtonWidth = iconButtonWidth, + _shapeVariant = shapeVariant, + super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; + final ButtonSizeVariant? _sizeVariant; + final IconButtonWidthVariant? _iconButtonWidth; + final ButtonShapeVariant? _shapeVariant; + + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + ButtonSizeVariant get sizeVariant => _sizeVariant ?? ButtonSizeVariant.small; + + @override + IconButtonWidthVariant get iconButtonWidth => _iconButtonWidth ?? IconButtonWidthVariant.standard; + + @override + ButtonShapeVariant get shapeVariant => _shapeVariant ?? ButtonShapeVariant.round; + + @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 (sizeVariant) { + ButtonSizeVariant.xSmall => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 6.0, 4.0, 6.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB(6.0, 6.0, 6.0, 6.0), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(10.0, 6.0, 10.0, 6.0), + }, + ButtonSizeVariant.small => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 8.0, 4.0, 8.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB(8.0, 8.0, 8.0, 8.0), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(14.0, 8.0, 14.0, 8.0), + }, + ButtonSizeVariant.medium => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(12.0, 16.0, 12.0, 16.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB( + 16.0, + 16.0, + 16.0, + 16.0, + ), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(24.0, 16.0, 24.0, 16.0), + }, + ButtonSizeVariant.large => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(16.0, 32.0, 16.0, 32.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB( + 32.0, + 32.0, + 32.0, + 32.0, + ), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(48.0, 32.0, 48.0, 32.0), + }, + ButtonSizeVariant.xLarge => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(32.0, 48.0, 32.0, 48.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB( + 48.0, + 48.0, + 48.0, + 48.0, + ), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(72.0, 48.0, 72.0, 48.0), + }, + }); + + @override + WidgetStateProperty? get minimumSize => + MaterialStatePropertyAll(switch (sizeVariant) { + ButtonSizeVariant.xSmall => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(28.0, 32.0), + IconButtonWidthVariant.standard => const Size(32.0, 32.0), + IconButtonWidthVariant.wide => const Size(40.0, 32.0), + }, + ButtonSizeVariant.small => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(32.0, 40.0), + IconButtonWidthVariant.standard => const Size(40.0, 40.0), + IconButtonWidthVariant.wide => const Size(52.0, 40.0), + }, + ButtonSizeVariant.medium => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(48.0, 56.0), + IconButtonWidthVariant.standard => const Size(56.0, 56.0), + IconButtonWidthVariant.wide => const Size(72.0, 56.0), + }, + ButtonSizeVariant.large => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(64.0, 96.0), + IconButtonWidthVariant.standard => const Size(96.0, 96.0), + IconButtonWidthVariant.wide => const Size(128.0, 96.0), + }, + ButtonSizeVariant.xLarge => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(104.0, 136.0), + IconButtonWidthVariant.standard => const Size(136.0, 136.0), + IconButtonWidthVariant.wide => const Size(184.0, 136.0), + }, + }); + + @override + WidgetStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); + + @override + WidgetStateProperty? get iconSize => + MaterialStatePropertyAll(switch (sizeVariant) { + ButtonSizeVariant.xSmall => 20.0, + ButtonSizeVariant.small => 24.0, + ButtonSizeVariant.medium => 24.0, + ButtonSizeVariant.large => 32.0, + ButtonSizeVariant.xLarge => 40.0, + }); + + @override + WidgetStateProperty? get shape => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.pressed)) { + return switch (sizeVariant) { + ButtonSizeVariant.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSizeVariant.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSizeVariant.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSizeVariant.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + }; + } + if (toggleable && states.contains(WidgetState.selected)) { + return switch (shapeVariant) { + ButtonShapeVariant.round => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSizeVariant.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + ButtonSizeVariant.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + }, + ButtonShapeVariant.square => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const StadiumBorder(), + ButtonSizeVariant.small => const StadiumBorder(), + ButtonSizeVariant.medium => const StadiumBorder(), + ButtonSizeVariant.large => const StadiumBorder(), + ButtonSizeVariant.xLarge => const StadiumBorder(), + }, + }; + } + return switch (shapeVariant) { + ButtonShapeVariant.round => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const StadiumBorder(), + ButtonSizeVariant.small => const StadiumBorder(), + ButtonSizeVariant.medium => const StadiumBorder(), + ButtonSizeVariant.large => const StadiumBorder(), + ButtonSizeVariant.xLarge => const StadiumBorder(), + }, + ButtonShapeVariant.square => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSizeVariant.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + ButtonSizeVariant.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + }, + }; + }); + + @override + WidgetStateProperty? get side => null; + + @override + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; + + @override + VisualDensity? get visualDensity => VisualDensity.standard; + + @override + MaterialTapTargetSize? get tapTargetSize => MaterialTapTargetSize.padded; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} + +class _OutlinedIconButtonDefaultsM3E extends ButtonStyle { + _OutlinedIconButtonDefaultsM3E( + this.context, + this.toggleable, + ButtonSizeVariant? sizeVariant, + IconButtonWidthVariant? iconButtonWidth, + ButtonShapeVariant? shapeVariant, + ) : _sizeVariant = sizeVariant, + _iconButtonWidth = iconButtonWidth, + _shapeVariant = shapeVariant, + super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; + final ButtonSizeVariant? _sizeVariant; + final IconButtonWidthVariant? _iconButtonWidth; + final ButtonShapeVariant? _shapeVariant; + + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + ButtonSizeVariant get sizeVariant => _sizeVariant ?? ButtonSizeVariant.small; + + @override + IconButtonWidthVariant get iconButtonWidth => _iconButtonWidth ?? IconButtonWidthVariant.standard; + + @override + ButtonShapeVariant get shapeVariant => _shapeVariant ?? ButtonShapeVariant.round; + + @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 (sizeVariant) { + ButtonSizeVariant.xSmall => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 6.0, 4.0, 6.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB(6.0, 6.0, 6.0, 6.0), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(10.0, 6.0, 10.0, 6.0), + }, + ButtonSizeVariant.small => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(4.0, 8.0, 4.0, 8.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB(8.0, 8.0, 8.0, 8.0), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(14.0, 8.0, 14.0, 8.0), + }, + ButtonSizeVariant.medium => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(12.0, 16.0, 12.0, 16.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB( + 16.0, + 16.0, + 16.0, + 16.0, + ), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(24.0, 16.0, 24.0, 16.0), + }, + ButtonSizeVariant.large => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(16.0, 32.0, 16.0, 32.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB( + 32.0, + 32.0, + 32.0, + 32.0, + ), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(48.0, 32.0, 48.0, 32.0), + }, + ButtonSizeVariant.xLarge => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB(32.0, 48.0, 32.0, 48.0), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB( + 48.0, + 48.0, + 48.0, + 48.0, + ), + IconButtonWidthVariant.wide => const EdgeInsetsDirectional.fromSTEB(72.0, 48.0, 72.0, 48.0), + }, + }); + + @override + WidgetStateProperty? get minimumSize => + MaterialStatePropertyAll(switch (sizeVariant) { + ButtonSizeVariant.xSmall => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(28.0, 32.0), + IconButtonWidthVariant.standard => const Size(32.0, 32.0), + IconButtonWidthVariant.wide => const Size(40.0, 32.0), + }, + ButtonSizeVariant.small => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(32.0, 40.0), + IconButtonWidthVariant.standard => const Size(40.0, 40.0), + IconButtonWidthVariant.wide => const Size(52.0, 40.0), + }, + ButtonSizeVariant.medium => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(48.0, 56.0), + IconButtonWidthVariant.standard => const Size(56.0, 56.0), + IconButtonWidthVariant.wide => const Size(72.0, 56.0), + }, + ButtonSizeVariant.large => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(64.0, 96.0), + IconButtonWidthVariant.standard => const Size(96.0, 96.0), + IconButtonWidthVariant.wide => const Size(128.0, 96.0), + }, + ButtonSizeVariant.xLarge => switch (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(104.0, 136.0), + IconButtonWidthVariant.standard => const Size(136.0, 136.0), + IconButtonWidthVariant.wide => const Size(184.0, 136.0), + }, + }); + + @override + WidgetStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); + + @override + WidgetStateProperty? get iconSize => + MaterialStatePropertyAll(switch (sizeVariant) { + ButtonSizeVariant.xSmall => 20.0, + ButtonSizeVariant.small => 24.0, + ButtonSizeVariant.medium => 24.0, + ButtonSizeVariant.large => 32.0, + ButtonSizeVariant.xLarge => 40.0, + }); + + @override + WidgetStateProperty? get shape => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.pressed)) { + return switch (sizeVariant) { + ButtonSizeVariant.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSizeVariant.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ButtonSizeVariant.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSizeVariant.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + }; + } + if (toggleable && states.contains(WidgetState.selected)) { + return switch (shapeVariant) { + ButtonShapeVariant.round => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSizeVariant.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + ButtonSizeVariant.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + }, + ButtonShapeVariant.square => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const StadiumBorder(), + ButtonSizeVariant.small => const StadiumBorder(), + ButtonSizeVariant.medium => const StadiumBorder(), + ButtonSizeVariant.large => const StadiumBorder(), + ButtonSizeVariant.xLarge => const StadiumBorder(), + }, + }; + } + return switch (shapeVariant) { + ButtonShapeVariant.round => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const StadiumBorder(), + ButtonSizeVariant.small => const StadiumBorder(), + ButtonSizeVariant.medium => const StadiumBorder(), + ButtonSizeVariant.large => const StadiumBorder(), + ButtonSizeVariant.xLarge => const StadiumBorder(), + }, + ButtonShapeVariant.square => switch (sizeVariant) { + ButtonSizeVariant.xSmall => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.small => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + ButtonSizeVariant.medium => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ButtonSizeVariant.large => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + ButtonSizeVariant.xLarge => const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0)), + ), + }, + }; + }); + + @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 (sizeVariant) { + ButtonSizeVariant.xSmall => 1.0, + ButtonSizeVariant.small => 1.0, + ButtonSizeVariant.medium => 1.0, + ButtonSizeVariant.large => 2.0, + ButtonSizeVariant.xLarge => 3.0, + }, + ); + } + return BorderSide( + color: _colors.outlineVariant, + width: switch (sizeVariant) { + ButtonSizeVariant.xSmall => 1.0, + ButtonSizeVariant.small => 1.0, + ButtonSizeVariant.medium => 1.0, + ButtonSizeVariant.large => 2.0, + ButtonSizeVariant.xLarge => 3.0, + }, + ); + }); + + @override + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; + + @override + VisualDensity? get visualDensity => VisualDensity.standard; + + @override + MaterialTapTargetSize? get tapTargetSize => MaterialTapTargetSize.padded; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} diff --git a/packages/material_ui/lib/src/icon_button.dart b/packages/material_ui/lib/src/icon_button.dart index 9f026b7aa3dc..d4b48314c1aa 100644 --- a/packages/material_ui/lib/src/icon_button.dart +++ b/packages/material_ui/lib/src/icon_button.dart @@ -31,6 +31,8 @@ import 'theme.dart'; import 'theme_data.dart'; import 'tooltip.dart'; +part 'generated/icon_button_defaults_m3e.g.dart'; + // Examples can assume: // late BuildContext context; @@ -626,6 +628,17 @@ class IconButton extends StatelessWidget { /// create a [WidgetStateProperty] with a single value for all /// states. /// + /// The [sizeVariant], [iconButtonWidth], and [shapeVariant] parameters are + /// Material 3 Expressive options. They provide extra-small through + /// extra-large sizes, narrow through wide widths, and round or square shapes + /// through [ButtonStyle.sizeVariant], [ButtonStyle.iconButtonWidth], and + /// [ButtonStyle.shapeVariant]. + /// + /// When [IconButtonThemeData.variant] is [StyleVariant.material3Expressive], + /// and these properties are null, Material 3 Expressive [IconButton] defaults + /// use [ButtonSizeVariant.small], [IconButtonWidthVariant.standard], and + /// [ButtonShapeVariant.round]. + /// /// All parameters default to null, by default this method returns /// a [ButtonStyle] that doesn't override anything. /// @@ -670,6 +683,9 @@ class IconButton extends StatelessWidget { bool? enableFeedback, AlignmentGeometry? alignment, InteractiveInkFeatureFactory? splashFactory, + ButtonSizeVariant? sizeVariant, + IconButtonWidthVariant? iconButtonWidth, + ButtonShapeVariant? shapeVariant, }) { final Color? overlayFallback = overlayColor ?? foregroundColor; WidgetStateProperty? overlayColorProp; @@ -710,6 +726,9 @@ class IconButton extends StatelessWidget { enableFeedback: enableFeedback, alignment: alignment, splashFactory: splashFactory, + sizeVariant: sizeVariant, + iconButtonWidth: iconButtonWidth, + shapeVariant: shapeVariant, ); } @@ -718,6 +737,9 @@ class IconButton extends StatelessWidget { final ThemeData theme = Theme.of(context); if (theme.useMaterial3) { + final StyleVariant effectiveVariant = + IconButtonTheme.of(context).variant ?? StyleVariant.material3; + final Size? minSize = constraints == null ? null : Size(constraints!.minWidth, constraints!.minHeight); @@ -761,6 +783,7 @@ class IconButton extends StatelessWidget { focusNode: focusNode, isSelected: isSelected, variant: _variant, + styleVariant: effectiveVariant, tooltip: tooltip, statesController: statesController, child: effectiveIcon, @@ -867,6 +890,7 @@ class _SelectableIconButton extends StatefulWidget { this.onHover, this.statesController, required this.variant, + required this.styleVariant, required this.autofocus, required this.onPressed, this.tooltip, @@ -877,6 +901,7 @@ class _SelectableIconButton extends StatefulWidget { final ButtonStyle? style; final FocusNode? focusNode; final _IconButtonVariant variant; + final StyleVariant styleVariant; final bool autofocus; final VoidCallback? onPressed; final String? tooltip; @@ -945,7 +970,8 @@ class _SelectableIconButtonState extends State<_SelectableIconButton> { onPressed: widget.onPressed, onHover: widget.onHover, onLongPress: widget.onPressed != null ? widget.onLongPress : null, - variant: widget.variant, + iconButtonVariant: widget.variant, + styleVariant: widget.styleVariant, toggleable: toggleable, tooltip: widget.tooltip, child: Semantics(selected: widget.isSelected, child: widget.child), @@ -968,19 +994,23 @@ class _IconButtonM3 extends ButtonStyleButton { super.onLongPress, super.autofocus = false, super.statesController, - required this.variant, + required this.iconButtonVariant, + required this.styleVariant, required this.toggleable, super.tooltip, required Widget super.child, }) : super(onFocusChange: null, clipBehavior: Clip.none); - final _IconButtonVariant variant; + final _IconButtonVariant iconButtonVariant; + final StyleVariant styleVariant; final bool toggleable; /// ## Material 3 defaults /// - /// If [ThemeData.useMaterial3] is set to true the following defaults will - /// be used: + // TODO(quncCccccc): Clean up [ThemeData.useMaterial3] once useMaterial3 is deprecated. + /// If [ThemeData.useMaterial3] is true and [IconButtonThemeData.variant] is + /// [StyleVariant.material3], the following defaults will be + /// used: /// /// * `textStyle` - null /// * `backgroundColor` - transparent @@ -1012,13 +1042,96 @@ class _IconButtonM3 extends ButtonStyleButton { /// * `enableFeedback` - true /// * `alignment` - Alignment.center /// * `splashFactory` - Theme.splashFactory + /// + /// ## Material 3 Expressive defaults + /// + /// If [ThemeData.useMaterial3] is true and [IconButtonThemeData.variant] is + /// [StyleVariant.material3Expressive], Material 3 Expressive defaults are + /// used: + /// + /// * `textStyle` - null + /// * `backgroundColor` - transparent + /// * `foregroundColor` + /// * disabled - Theme.colorScheme.onSurface(0.38) + /// * selected - Theme.colorScheme.primary + /// * others - Theme.colorScheme.onSurfaceVariant + /// * `overlayColor` + /// * selected + /// * hovered - Theme.colorScheme.primary(0.08) + /// * focused or pressed - Theme.colorScheme.primary(0.1) + /// * hovered - Theme.colorScheme.onSurfaceVariant(0.08) + /// * pressed or focused - Theme.colorScheme.onSurfaceVariant(0.1) + /// * others - transparent + /// * `shadowColor` - null + /// * `surfaceTintColor` - null + /// * `elevation` - 0 + /// * `padding` - based on [ButtonStyle.sizeVariant] and + /// [ButtonStyle.iconButtonWidth]; defaults to all(8) + /// * `minimumSize` - based on [ButtonStyle.sizeVariant] and + /// [ButtonStyle.iconButtonWidth]; defaults to Size(40, 40) + /// * `fixedSize` - null + /// * `maximumSize` - Size.infinite + /// * `iconSize` - based on [ButtonStyle.sizeVariant]; defaults to 24 + /// * `side` - null + /// * `shape` - based on [ButtonStyle.sizeVariant], + /// [ButtonStyle.shapeVariant], and state; defaults to StadiumBorder() + /// * `mouseCursor` - WidgetStateMouseCursor.adaptiveClickable + /// * `visualDensity` - VisualDensity.standard + /// * `tapTargetSize` - MaterialTapTargetSize.padded + /// * `animationDuration` - kThemeChangeDuration + /// * `enableFeedback` - true + /// * `alignment` - Alignment.center + /// * `splashFactory` - Theme.splashFactory + /// * `sizeVariant` - ButtonSizeVariant.small + /// * `iconButtonWidth` - IconButtonWidthVariant.standard + /// * `shapeVariant` - ButtonShapeVariant.round @override ButtonStyle defaultStyleOf(BuildContext context) { - return switch (variant) { - _IconButtonVariant.filled => _FilledIconButtonDefaultsM3(context, toggleable), - _IconButtonVariant.filledTonal => _FilledTonalIconButtonDefaultsM3(context, toggleable), - _IconButtonVariant.outlined => _OutlinedIconButtonDefaultsM3(context, toggleable), - _IconButtonVariant.standard => _IconButtonDefaultsM3(context, toggleable), + final ButtonStyle? iconButtonThemeStyle = IconButtonTheme.of(context).style; + final ButtonSizeVariant? effectiveSize = + style?.sizeVariant ?? iconButtonThemeStyle?.sizeVariant; + final IconButtonWidthVariant? effectiveWidth = + style?.iconButtonWidth ?? iconButtonThemeStyle?.iconButtonWidth; + final ButtonShapeVariant? effectiveShape = + style?.shapeVariant ?? iconButtonThemeStyle?.shapeVariant; + + return switch (styleVariant) { + StyleVariant.material3 => switch (iconButtonVariant) { + _IconButtonVariant.filled => _FilledIconButtonDefaultsM3(context, toggleable), + _IconButtonVariant.filledTonal => _FilledTonalIconButtonDefaultsM3(context, toggleable), + _IconButtonVariant.outlined => _OutlinedIconButtonDefaultsM3(context, toggleable), + _IconButtonVariant.standard => _IconButtonDefaultsM3(context, toggleable), + }, + StyleVariant.material3Expressive => switch (iconButtonVariant) { + _IconButtonVariant.filled => _FilledIconButtonDefaultsM3E( + context, + toggleable, + effectiveSize, + effectiveWidth, + effectiveShape, + ), + _IconButtonVariant.filledTonal => _FilledTonalIconButtonDefaultsM3E( + context, + toggleable, + effectiveSize, + effectiveWidth, + effectiveShape, + ), + _IconButtonVariant.outlined => _OutlinedIconButtonDefaultsM3E( + context, + toggleable, + effectiveSize, + effectiveWidth, + effectiveShape, + ), + _IconButtonVariant.standard => _IconButtonDefaultsM3E( + context, + toggleable, + effectiveSize, + effectiveWidth, + effectiveShape, + ), + }, }; } diff --git a/packages/material_ui/lib/src/icon_button_theme.dart b/packages/material_ui/lib/src/icon_button_theme.dart index 0d38686dba0f..1d5905b74cf0 100644 --- a/packages/material_ui/lib/src/icon_button_theme.dart +++ b/packages/material_ui/lib/src/icon_button_theme.dart @@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart'; import 'button_style.dart'; import 'theme.dart'; +import 'theme_data.dart'; // Examples can assume: // late BuildContext context; @@ -39,7 +40,7 @@ class IconButtonThemeData with Diagnosticable { /// Creates a [IconButtonThemeData]. /// /// The [style] may be null. - const IconButtonThemeData({this.style}); + const IconButtonThemeData({this.style, this.variant}); /// Overrides for [IconButton]'s default style if [ThemeData.useMaterial3] /// is set to true. @@ -50,16 +51,27 @@ class IconButtonThemeData with Diagnosticable { /// If [style] is null, then this theme doesn't override anything. final ButtonStyle? style; + /// The style variant of Material Design used by [IconButton]. + /// + /// Set this to [StyleVariant.material3Expressive] to opt icon buttons into + /// the Material 3 Expressive style. + /// + /// If null, [IconButton] uses [StyleVariant.material3]. + final StyleVariant? variant; + /// Linearly interpolate between two icon button themes. static IconButtonThemeData? lerp(IconButtonThemeData? a, IconButtonThemeData? b, double t) { if (identical(a, b)) { return a; } - return IconButtonThemeData(style: ButtonStyle.lerp(a?.style, b?.style, t)); + return IconButtonThemeData( + style: ButtonStyle.lerp(a?.style, b?.style, t), + variant: t < 0.5 ? a?.variant : b?.variant, + ); } @override - int get hashCode => style.hashCode; + int get hashCode => Object.hash(style, variant); @override bool operator ==(Object other) { @@ -69,13 +81,14 @@ class IconButtonThemeData with Diagnosticable { if (other.runtimeType != runtimeType) { return false; } - return other is IconButtonThemeData && other.style == style; + return other is IconButtonThemeData && other.style == style && other.variant == variant; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('style', style, defaultValue: null)); + properties.add(EnumProperty('variant', variant, defaultValue: null)); } } diff --git a/packages/material_ui/test/icon_button_test.dart b/packages/material_ui/test/icon_button_test.dart index 1bb186526103..3b0bc7412d39 100644 --- a/packages/material_ui/test/icon_button_test.dart +++ b/packages/material_ui/test/icon_button_test.dart @@ -3571,6 +3571,760 @@ void main() { ); expect(tester.getSize(find.byType(IconButton)), Size.zero); }); + + Widget buildM3EApp({required Widget child, ThemeData? theme}) { + return MaterialApp( + theme: + theme ?? + ThemeData( + iconButtonTheme: const IconButtonThemeData(variant: StyleVariant.material3Expressive), + ), + home: Scaffold(body: Center(child: child)), + ); + } + + Finder m3eIconButtonMaterialFinder() { + return find.descendant(of: find.byType(IconButton), matching: find.byType(Material)); + } + + Material m3eIconButtonMaterial(WidgetTester tester) { + return tester.widget(m3eIconButtonMaterialFinder()); + } + + Size m3eIconButtonMaterialSize(WidgetTester tester) { + return tester.getSize(m3eIconButtonMaterialFinder()); + } + + ColorScheme m3eColorScheme(WidgetTester tester) { + return Theme.of(tester.element(find.byType(IconButton))).colorScheme; + } + + Color? m3eIconColor(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( + buildM3EApp( + child: IconButton(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + // ButtonStyleButton renders with minimum size 40x40, but tap target + // padding brings it to 48x48. + expect(m3eIconButtonMaterialSize(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 with 48dp tap target', ( + WidgetTester tester, + ) async { + var pressCount = 0; + await tester.pumpWidget( + buildM3EApp( + child: IconButton( + onPressed: () { + pressCount += 1; + }, + icon: const Icon(Icons.add), + style: const ButtonStyle(sizeVariant: ButtonSizeVariant.xSmall), + ), + ), + ); + + expect(m3eIconButtonMaterialSize(tester), const Size(32.0, 32.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(48.0, 48.0)); + + final Offset center = tester.getCenter(find.byType(IconButton)); + for (final offset in const [ + Offset(23.0, 0.0), + Offset(-23.0, 0.0), + Offset(0.0, 23.0), + Offset(0.0, -23.0), + ]) { + await tester.tapAt(center + offset); + await tester.pump(); + } + + expect(pressCount, 4); + }); + + testWidgets('styleFrom sets the size variant', (WidgetTester tester) async { + await tester.pumpWidget( + buildM3EApp( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: IconButton.styleFrom(sizeVariant: ButtonSizeVariant.medium), + ), + ), + ); + + expect(m3eIconButtonMaterialSize(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( + buildM3EApp( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(sizeVariant: ButtonSizeVariant.medium), + ), + ), + ); + + expect(m3eIconButtonMaterialSize(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( + buildM3EApp( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(sizeVariant: ButtonSizeVariant.large), + ), + ), + ); + + expect(m3eIconButtonMaterialSize(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( + buildM3EApp( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(sizeVariant: ButtonSizeVariant.xLarge), + ), + ), + ); + + expect(m3eIconButtonMaterialSize(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(IconButtonWidthVariant width) async { + await tester.pumpWidget( + buildM3EApp( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: ButtonStyle(iconButtonWidth: width), + ), + ), + ); + return m3eIconButtonMaterialSize(tester); + } + + expect(await materialSizeFor(IconButtonWidthVariant.narrow), const Size(32.0, 40.0)); + expect(await materialSizeFor(IconButtonWidthVariant.standard), const Size(40.0, 40.0)); + expect(await materialSizeFor(IconButtonWidthVariant.wide), const Size(52.0, 40.0)); + + expect(m3eIconButtonMaterial(tester).animationDuration, kThemeChangeDuration); + }); + + testWidgets('IconButtonThemeData style width sets default width', (WidgetTester tester) async { + await tester.pumpWidget( + buildM3EApp( + theme: ThemeData( + iconButtonTheme: const IconButtonThemeData( + style: ButtonStyle(iconButtonWidth: IconButtonWidthVariant.wide), + variant: StyleVariant.material3Expressive, + ), + ), + child: IconButton(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + expect(m3eIconButtonMaterialSize(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( + buildM3EApp( + 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('square shape resolves M3E token shapes by state', (WidgetTester tester) async { + final statesController = MaterialStatesController(); + await tester.pumpWidget( + buildM3EApp( + child: IconButton( + statesController: statesController, + isSelected: false, + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle( + sizeVariant: ButtonSizeVariant.medium, + shapeVariant: ButtonShapeVariant.square, + ), + ), + ), + ); + + expect( + materialShape(tester), + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))), + ); + + statesController.update(WidgetState.pressed, true); + await tester.pumpAndSettle(); + + expect( + materialShape(tester), + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0))), + ); + + statesController.update(WidgetState.pressed, false); + await tester.pumpWidget( + buildM3EApp( + child: IconButton( + statesController: statesController, + isSelected: true, + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle( + sizeVariant: ButtonSizeVariant.medium, + shapeVariant: ButtonShapeVariant.square, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(materialShape(tester), const StadiumBorder()); + statesController.dispose(); + }); + + testWidgets('ButtonStyle.shape remains the stateful shape override API', ( + WidgetTester tester, + ) async { + final statesController = MaterialStatesController(); + await tester.pumpWidget( + buildM3EApp( + 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( + buildM3EApp( + child: IconButton(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + expect(m3eIconButtonMaterial(tester).color, Colors.transparent); + expect(m3eIconButtonMaterial(tester).shape, const StadiumBorder()); + }); + + testWidgets('filled variant resolves default container color', (WidgetTester tester) async { + await tester.pumpWidget( + buildM3EApp( + child: IconButton.filled(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + expect(m3eIconButtonMaterial(tester).color, m3eColorScheme(tester).primary); + expect(m3eIconButtonMaterial(tester).shape, const StadiumBorder()); + }); + + testWidgets('filledTonal variant resolves default container color', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + buildM3EApp( + child: IconButton.filledTonal(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + expect(m3eIconButtonMaterial(tester).color, m3eColorScheme(tester).secondaryContainer); + expect(m3eIconButtonMaterial(tester).shape, const StadiumBorder()); + }); + + testWidgets('outlined variant resolves default side and transparent background', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + buildM3EApp( + child: IconButton.outlined(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + final shape = m3eIconButtonMaterial(tester).shape! as StadiumBorder; + expect(m3eIconButtonMaterial(tester).color, Colors.transparent); + expect(shape.side, BorderSide(color: m3eColorScheme(tester).outlineVariant)); + }); + + testWidgets('filled variant with style size', (WidgetTester tester) async { + await tester.pumpWidget( + buildM3EApp( + child: IconButton.filled( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(sizeVariant: ButtonSizeVariant.large), + ), + ), + ); + + expect(m3eIconButtonMaterialSize(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( + buildM3EApp( + child: IconButton.outlined( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(sizeVariant: ButtonSizeVariant.medium), + ), + ), + ); + + expect(m3eIconButtonMaterialSize(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( + buildM3EApp( + theme: ThemeData( + iconButtonTheme: const IconButtonThemeData( + style: ButtonStyle(sizeVariant: ButtonSizeVariant.large), + variant: StyleVariant.material3Expressive, + ), + ), + child: IconButton(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ); + + expect(m3eIconButtonMaterialSize(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( + buildM3EApp( + theme: ThemeData( + iconButtonTheme: const IconButtonThemeData( + style: ButtonStyle(sizeVariant: ButtonSizeVariant.large), + variant: StyleVariant.material3Expressive, + ), + ), + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(sizeVariant: ButtonSizeVariant.xSmall), + ), + ), + ); + + expect(m3eIconButtonMaterialSize(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( + buildM3EApp( + child: IconButtonTheme( + data: const IconButtonThemeData( + style: ButtonStyle(sizeVariant: ButtonSizeVariant.medium), + variant: StyleVariant.material3Expressive, + ), + child: IconButton(onPressed: () {}, icon: const Icon(Icons.add)), + ), + ), + ); + + expect(m3eIconButtonMaterialSize(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( + buildM3EApp( + 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( + buildM3EApp( + 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( + buildM3EApp( + 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( + buildM3EApp( + 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( + buildM3EApp( + 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( + buildM3EApp( + 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( + buildM3EApp( + 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( + buildM3EApp( + 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( + buildM3EApp(child: const IconButton(onPressed: null, icon: Icon(Icons.add))), + ); + + expect(m3eIconButtonMaterial(tester).color, Colors.transparent); + expect(m3eIconColor(tester, Icons.add), m3eColorScheme(tester).onSurface.withOpacity(0.38)); + }); + + testWidgets('onLongPress without onPressed keeps button disabled', (WidgetTester tester) async { + var longPressed = false; + final SemanticsHandle handle = tester.ensureSemantics(); + + await tester.pumpWidget( + buildM3EApp( + child: IconButton( + onPressed: null, + onLongPress: () { + longPressed = true; + }, + icon: const Icon(Icons.add, semanticLabel: 'add'), + ), + ), + ); + + expect( + tester.getSemantics(find.byType(IconButton)), + matchesSemantics(hasEnabledState: true, isButton: true, label: 'add'), + ); + + await tester.longPress(find.byType(IconButton)); + expect(longPressed, isFalse); + handle.dispose(); + }); + + testWidgets('disabled filled button has reduced background', (WidgetTester tester) async { + await tester.pumpWidget( + buildM3EApp(child: const IconButton.filled(onPressed: null, icon: Icon(Icons.add))), + ); + + expect( + m3eIconButtonMaterial(tester).color, + m3eColorScheme(tester).onSurface.withOpacity(0.1), + ); + expect(m3eIconColor(tester, Icons.add), m3eColorScheme(tester).onSurface.withOpacity(0.38)); + }); + }); + + group('IconButtonThemeData', () { + test('equality', () { + const a = IconButtonThemeData( + style: ButtonStyle( + sizeVariant: ButtonSizeVariant.small, + iconButtonWidth: IconButtonWidthVariant.standard, + shapeVariant: ButtonShapeVariant.round, + ), + ); + const b = IconButtonThemeData( + style: ButtonStyle( + sizeVariant: ButtonSizeVariant.small, + iconButtonWidth: IconButtonWidthVariant.standard, + shapeVariant: ButtonShapeVariant.round, + ), + ); + const c = IconButtonThemeData( + style: ButtonStyle( + sizeVariant: ButtonSizeVariant.large, + iconButtonWidth: IconButtonWidthVariant.wide, + shapeVariant: ButtonShapeVariant.square, + ), + ); + + expect(a, equals(b)); + expect(a, isNot(equals(c))); + }); + + test('hashCode', () { + const a = IconButtonThemeData( + style: ButtonStyle( + sizeVariant: ButtonSizeVariant.small, + iconButtonWidth: IconButtonWidthVariant.narrow, + shapeVariant: ButtonShapeVariant.round, + ), + ); + const b = IconButtonThemeData( + style: ButtonStyle( + sizeVariant: ButtonSizeVariant.small, + iconButtonWidth: IconButtonWidthVariant.narrow, + shapeVariant: ButtonShapeVariant.round, + ), + ); + + expect(a.hashCode, equals(b.hashCode)); + }); + + test('lerp', () { + const a = IconButtonThemeData( + style: ButtonStyle( + sizeVariant: ButtonSizeVariant.small, + iconButtonWidth: IconButtonWidthVariant.narrow, + shapeVariant: ButtonShapeVariant.round, + ), + ); + const b = IconButtonThemeData( + style: ButtonStyle( + sizeVariant: ButtonSizeVariant.large, + iconButtonWidth: IconButtonWidthVariant.wide, + shapeVariant: ButtonShapeVariant.square, + ), + ); + + expect(IconButtonThemeData.lerp(a, b, 0.0)?.style?.sizeVariant, ButtonSizeVariant.small); + expect(IconButtonThemeData.lerp(a, b, 0.4)?.style?.sizeVariant, ButtonSizeVariant.small); + expect(IconButtonThemeData.lerp(a, b, 0.5)?.style?.sizeVariant, ButtonSizeVariant.large); + expect(IconButtonThemeData.lerp(a, b, 1.0)?.style?.sizeVariant, ButtonSizeVariant.large); + expect( + IconButtonThemeData.lerp(a, b, 0.4)?.style?.iconButtonWidth, + IconButtonWidthVariant.narrow, + ); + expect( + IconButtonThemeData.lerp(a, b, 0.5)?.style?.iconButtonWidth, + IconButtonWidthVariant.wide, + ); + expect(IconButtonThemeData.lerp(a, b, 0.4)?.style?.shapeVariant, ButtonShapeVariant.round); + expect(IconButtonThemeData.lerp(a, b, 0.5)?.style?.shapeVariant, ButtonShapeVariant.square); + }); + + test('debugFillProperties includes size variant, width, and shape variant', () { + const data = IconButtonThemeData( + style: ButtonStyle( + sizeVariant: ButtonSizeVariant.medium, + iconButtonWidth: IconButtonWidthVariant.wide, + shapeVariant: ButtonShapeVariant.square, + ), + ); + 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('sizeVariant: medium'))); + expect(descriptions, contains(contains('iconButtonWidth: wide'))); + expect(descriptions, contains(contains('shapeVariant: square'))); + }); + }); + + group('M3E IconButton variant opt in', () { + testWidgets('IconButtonThemeData variant enables M3E defaults', (WidgetTester tester) async { + await tester.pumpWidget( + buildM3EApp( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.add), + style: const ButtonStyle(sizeVariant: ButtonSizeVariant.medium), + ), + ), + ); + + expect(m3eIconButtonMaterialSize(tester), const Size(56.0, 56.0)); + expect(tester.getSize(find.byType(IconButton)), const Size(56.0, 56.0)); + }); + }); } Widget buildAllVariants({ diff --git a/packages/material_ui/test/icon_button_theme_test.dart b/packages/material_ui/test/icon_button_theme_test.dart index c271ba758bd4..c4671671f0c6 100644 --- a/packages/material_ui/test/icon_button_theme_test.dart +++ b/packages/material_ui/test/icon_button_theme_test.dart @@ -20,6 +20,36 @@ void main() { expect(identical(IconButtonThemeData.lerp(data, data, 0.5), data), true); }); + test('IconButtonThemeData supports Material 3 Expressive variant', () { + const data = IconButtonThemeData(variant: StyleVariant.material3Expressive); + + expect(data.variant, StyleVariant.material3Expressive); + }); + + test('IconButtonThemeData variant defaults to null', () { + const data = IconButtonThemeData(); + + expect(data.variant, isNull); + }); + + testWidgets('IconButton supports Material 3 Expressive style variants', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold( + body: IconButtonTheme( + data: IconButtonThemeData(variant: StyleVariant.material3Expressive), + child: IconButton(onPressed: null, icon: Icon(Icons.ac_unit)), + ), + ), + ), + ); + + expect(tester.takeException(), isNull); + }); + testWidgets('Passing no IconButtonTheme returns defaults', (WidgetTester tester) async { const colorScheme = ColorScheme.light(); await tester.pumpWidget( 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 07c1adee0f27..f11bbe98b87e 100644 --- a/packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart +++ b/packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart @@ -14,8 +14,8 @@ import 'package:args/args.dart'; import '../templates/action_chip_template.dart'; import '../templates/app_bar_template.dart'; -import '../templates/banner_template.dart'; import '../templates/badge_template.dart'; +import '../templates/banner_template.dart'; // import '../templates/bottom_app_bar_template.dart'; import '../templates/bottom_sheet_template.dart'; // import '../templates/button_template.dart'; @@ -30,7 +30,7 @@ import '../templates/bottom_sheet_template.dart'; // import '../templates/expansion_tile_template.dart'; // import '../templates/fab_template.dart'; // import '../templates/filter_chip_template.dart'; -// import '../templates/icon_button_template.dart'; +import '../templates/icon_button_template.dart'; // import '../templates/input_chip_template.dart'; // import '../templates/input_decorator_template.dart'; // import '../templates/list_tile_template.dart'; @@ -82,6 +82,7 @@ Future main(List args) async { // const FabTemplateM3().generateFile(verbose: verbose); // const FilterChipTemplateM3().generateFile(verbose: verbose); // const IconButtonTemplateM3().generateFile(verbose: verbose); + const IconButtonTemplateM3E().generateFile(verbose: verbose); // const InputChipTemplateM3().generateFile(verbose: verbose); // const InputDecoratorTemplateM3().generateFile(verbose: verbose); // const ListTileTemplateM3().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..1f9afd190373 --- /dev/null +++ b/packages/material_ui/tool/gen_defaults/templates/icon_button_template.dart @@ -0,0 +1,806 @@ +// 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/color_role.dart'; +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 IconButtonTemplateM3E extends TokenTemplateM3E { + const IconButtonTemplateM3E(); + + @override + String get name => 'Icon Button'; + + @override + String get parentFilePath => 'icon_button.dart'; + + String tokenColor(TokenColorRole role) { + return switch (role) { + TokenColorRole.inverseOnSurface => '_colors.onInverseSurface', + _ => color(role, '_colors'), + }; + } + + String componentColor(TokenColorRole role, double opacity) { + final String value = tokenColor(role); + if (opacity == 1.0) { + return value; + } + return '$value.withOpacity($opacity)'; + } + + @override + String generateContents(String className) { + return ''' +${_generateStandardDefaults(className)} +${_generateFilledDefaults()} +${_generateFilledTonalDefaults()} +${_generateOutlinedDefaults()} +'''; + } + + String _sizeSwitch({ + required String xSmall, + required String small, + required String medium, + required String large, + required String xLarge, + }) { + return ''' +switch (sizeVariant) { + ButtonSizeVariant.xSmall => $xSmall, + ButtonSizeVariant.small => $small, + ButtonSizeVariant.medium => $medium, + ButtonSizeVariant.large => $large, + ButtonSizeVariant.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 (iconButtonWidth) { + IconButtonWidthVariant.narrow => const EdgeInsetsDirectional.fromSTEB($narrowLeading, $defaultLeading, $narrowTrailing, $defaultTrailing), + IconButtonWidthVariant.standard => const EdgeInsetsDirectional.fromSTEB($defaultLeading, $defaultLeading, $defaultTrailing, $defaultTrailing), + IconButtonWidthVariant.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 (iconButtonWidth) { + IconButtonWidthVariant.narrow => const Size(${iconSize + narrowLeading + narrowTrailing}, $height), + IconButtonWidthVariant.standard => const Size(${iconSize + defaultLeading + defaultTrailing}, $height), + IconButtonWidthVariant.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 _containerRoundShapeSwitch { + return _sizeSwitch( + xSmall: shape(TokenIconButtonXsmall.containerShapeRound), + small: shape(TokenIconButtonSmall.containerShapeRound), + medium: shape(TokenIconButtonMedium.containerShapeRound), + large: shape(TokenIconButtonLarge.containerShapeRound), + xLarge: shape(TokenIconButtonXlarge.containerShapeRound), + ); + } + + String get _containerSquareShapeSwitch { + return _sizeSwitch( + xSmall: shape(TokenIconButtonXsmall.containerShapeSquare), + small: shape(TokenIconButtonSmall.containerShapeSquare), + medium: shape(TokenIconButtonMedium.containerShapeSquare), + large: shape(TokenIconButtonLarge.containerShapeSquare), + xLarge: shape(TokenIconButtonXlarge.containerShapeSquare), + ); + } + + String get _containerShapeSwitch { + return ''' +switch (shapeVariant) { + ButtonShapeVariant.round => $_containerRoundShapeSwitch, + ButtonShapeVariant.square => $_containerSquareShapeSwitch, + }'''; + } + + 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 _selectedRoundShapeSwitch { + return _sizeSwitch( + xSmall: shape(TokenIconButtonXsmall.selectedContainerShapeRound), + small: shape(TokenIconButtonSmall.selectedContainerShapeRound), + medium: shape(TokenIconButtonMedium.selectedContainerShapeRound), + large: shape(TokenIconButtonLarge.selectedContainerShapeRound), + xLarge: shape(TokenIconButtonXlarge.selectedContainerShapeRound), + ); + } + + String get _selectedSquareShapeSwitch { + return _sizeSwitch( + xSmall: shape(TokenIconButtonXsmall.selectedContainerShapeSquare), + small: shape(TokenIconButtonSmall.selectedContainerShapeSquare), + medium: shape(TokenIconButtonMedium.selectedContainerShapeSquare), + large: shape(TokenIconButtonLarge.selectedContainerShapeSquare), + xLarge: shape(TokenIconButtonXlarge.selectedContainerShapeSquare), + ); + } + + String get _selectedShapeSwitch { + return ''' +switch (shapeVariant) { + ButtonShapeVariant.round => $_selectedRoundShapeSwitch, + ButtonShapeVariant.square => $_selectedSquareShapeSwitch, + }'''; + } + + String get _variantFields { + return ''' + final ButtonSizeVariant? _sizeVariant; + final IconButtonWidthVariant? _iconButtonWidth; + final ButtonShapeVariant? _shapeVariant; +'''; + } + + String get _variantGetters { + return ''' + @override + ButtonSizeVariant get sizeVariant => _sizeVariant ?? ButtonSizeVariant.small; + + @override + IconButtonWidthVariant get iconButtonWidth => _iconButtonWidth ?? IconButtonWidthVariant.standard; + + @override + ButtonShapeVariant get shapeVariant => _shapeVariant ?? ButtonShapeVariant.round; +'''; + } + + String get _sizeDependentProperties { + return ''' + @override + WidgetStateProperty? get padding => + MaterialStatePropertyAll($_paddingSwitch); + + @override + WidgetStateProperty? get minimumSize => + MaterialStatePropertyAll($_minimumSizeSwitch); + + @override + WidgetStateProperty? get maximumSize => + const MaterialStatePropertyAll(Size.infinite); + + @override + WidgetStateProperty? get iconSize => + MaterialStatePropertyAll($_iconSizeSwitch); + + @override + WidgetStateProperty? get shape => + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.pressed)) { + return $_pressedShapeSwitch; + } + if (toggleable && states.contains(WidgetState.selected)) { + return $_selectedShapeSwitch; + } + return $_containerShapeSwitch; + }); +'''; + } + + String _generateStandardDefaults(String className) { + return ''' +class $className extends ButtonStyle { + $className( + this.context, + this.toggleable, + ButtonSizeVariant? sizeVariant, + IconButtonWidthVariant? iconButtonWidth, + ButtonShapeVariant? shapeVariant, + ) : _sizeVariant = sizeVariant, + _iconButtonWidth = iconButtonWidth, + _shapeVariant = shapeVariant, + super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; +$_variantFields + late final ColorScheme _colors = Theme.of(context).colorScheme; + +$_variantGetters + + @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 ${tokenColor(TokenIconButtonStandard.selectedIconColor)}; + } + return ${tokenColor(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 => MaterialTapTargetSize.padded; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} +'''; + } + + String _generateFilledDefaults() { + return ''' +class _FilledIconButtonDefaultsM3E extends ButtonStyle { + _FilledIconButtonDefaultsM3E( + this.context, + this.toggleable, + ButtonSizeVariant? sizeVariant, + IconButtonWidthVariant? iconButtonWidth, + ButtonShapeVariant? shapeVariant, + ) : _sizeVariant = sizeVariant, + _iconButtonWidth = iconButtonWidth, + _shapeVariant = shapeVariant, + super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; +$_variantFields + late final ColorScheme _colors = Theme.of(context).colorScheme; + +$_variantGetters + + @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 ${tokenColor(TokenIconButtonFilled.selectedContainerColor)}; + } + if (toggleable) { + return ${tokenColor(TokenIconButtonFilled.unselectedContainerColor)}; + } + return ${tokenColor(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 ${tokenColor(TokenIconButtonFilled.selectedIconColor)}; + } + if (toggleable) { + return ${tokenColor(TokenIconButtonFilled.unselectedIconColor)}; + } + return ${tokenColor(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 => MaterialTapTargetSize.padded; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} +'''; + } + + String _generateFilledTonalDefaults() { + return ''' +class _FilledTonalIconButtonDefaultsM3E extends ButtonStyle { + _FilledTonalIconButtonDefaultsM3E( + this.context, + this.toggleable, + ButtonSizeVariant? sizeVariant, + IconButtonWidthVariant? iconButtonWidth, + ButtonShapeVariant? shapeVariant, + ) : _sizeVariant = sizeVariant, + _iconButtonWidth = iconButtonWidth, + _shapeVariant = shapeVariant, + super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; +$_variantFields + late final ColorScheme _colors = Theme.of(context).colorScheme; + +$_variantGetters + + @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 ${tokenColor(TokenIconButtonTonal.selectedContainerColor)}; + } + if (toggleable) { + return ${tokenColor(TokenIconButtonTonal.unselectedContainerColor)}; + } + return ${tokenColor(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 ${tokenColor(TokenIconButtonTonal.selectedIconColor)}; + } + if (toggleable) { + return ${tokenColor(TokenIconButtonTonal.unselectedIconColor)}; + } + return ${tokenColor(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 => MaterialTapTargetSize.padded; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} +'''; + } + + String _generateOutlinedDefaults() { + return ''' +class _OutlinedIconButtonDefaultsM3E extends ButtonStyle { + _OutlinedIconButtonDefaultsM3E( + this.context, + this.toggleable, + ButtonSizeVariant? sizeVariant, + IconButtonWidthVariant? iconButtonWidth, + ButtonShapeVariant? shapeVariant, + ) : _sizeVariant = sizeVariant, + _iconButtonWidth = iconButtonWidth, + _shapeVariant = shapeVariant, + super( + animationDuration: kThemeChangeDuration, + enableFeedback: true, + alignment: Alignment.center, + ); + + final BuildContext context; + final bool toggleable; +$_variantFields + late final ColorScheme _colors = Theme.of(context).colorScheme; + +$_variantGetters + + @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 ${tokenColor(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 ${tokenColor(TokenIconButtonOutlined.selectedIconColor)}; + } + return ${tokenColor(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: ${tokenColor(TokenIconButtonOutlined.unselectedDisabledOutlineColor)}, width: $_outlineWidthSwitch); + } + return BorderSide(color: ${tokenColor(TokenIconButtonOutlined.outlineColor)}, width: $_outlineWidthSwitch); + }); + + @override + WidgetStateProperty? get mouseCursor => WidgetStateMouseCursor.adaptiveClickable; + + @override + VisualDensity? get visualDensity => VisualDensity.standard; + + @override + MaterialTapTargetSize? get tapTargetSize => MaterialTapTargetSize.padded; + + @override + InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; +} +'''; + } +}