un-break ThemeData equality (#154695)

This PR is _almost_ able to close issue #89127.

Sadly, no `InheritedModel` or custom `RenderObject`s today; instead the [WidgetState operators](https://main-api.flutter.dev/flutter/widgets/WidgetStateOperators.html) have been restructured to support equality checks.

`WidgetStateProperty.fromMap()` is now capable of accurate equality checks, and all of the `.styleFrom()` methods have been refactored to use that constructor.

(Equality checks are still broken for `WidgetStateProperty.resolveWith()`, and any other non-`const` objects that implement the interface.)

<br><br>

credit for this idea goes to @justinmc: https://github.com/flutter/flutter/issues/89127#issuecomment-2313187703
This commit is contained in:
Nate Wilson 2024-09-09 15:49:09 -06:00 committed by GitHub
parent 18c325af17
commit bfa04edca6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 360 additions and 564 deletions

View File

@ -120,31 +120,24 @@ class _${blockName}DefaultsM3 extends SegmentedButtonThemeData {
@override
Widget? get selectedIcon => const Icon(Icons.check);
static MaterialStateProperty<Color?> resolveStateColor(Color? unselectedColor, Color? selectedColor, Color? overlayColor){
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return (overlayColor ?? selectedColor)?.withOpacity(0.1);
}
if (states.contains(MaterialState.hovered)) {
return (overlayColor ?? selectedColor)?.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return (overlayColor ?? selectedColor)?.withOpacity(0.1);
}
} else {
if (states.contains(MaterialState.pressed)) {
return (overlayColor ?? unselectedColor)?.withOpacity(0.1);
}
if (states.contains(MaterialState.hovered)) {
return (overlayColor ?? unselectedColor)?.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return (overlayColor ?? unselectedColor)?.withOpacity(0.1);
}
}
return Colors.transparent;
});
static WidgetStateProperty<Color?> resolveStateColor(
Color? unselectedColor,
Color? selectedColor,
Color? overlayColor,
) {
final Color? selected = overlayColor ?? selectedColor;
final Color? unselected = overlayColor ?? unselectedColor;
return WidgetStateProperty<Color?>.fromMap(
<WidgetStatesConstraint, Color?>{
WidgetState.selected & WidgetState.pressed: selected?.withOpacity(0.1),
WidgetState.selected & WidgetState.hovered: selected?.withOpacity(0.08),
WidgetState.selected & WidgetState.focused: selected?.withOpacity(0.1),
WidgetState.pressed: unselected?.withOpacity(0.1),
WidgetState.hovered: unselected?.withOpacity(0.08),
WidgetState.focused: unselected?.withOpacity(0.1),
WidgetState.any: Colors.transparent,
},
);
}
}
''';

View File

@ -30,8 +30,8 @@ class MaterialStateExample extends StatelessWidget {
Widget build(BuildContext context) {
return TextFormField(
initialValue: 'abc',
decoration: InputDecoration(
prefixIcon: const Icon(Icons.person),
decoration: const InputDecoration(
prefixIcon: Icon(Icons.person),
prefixIconColor: WidgetStateColor.fromMap(
<WidgetStatesConstraint, Color>{
WidgetState.focused: Colors.green,

View File

@ -32,7 +32,7 @@ class MaterialStateExample extends StatelessWidget {
return Theme(
data: themeData.copyWith(
inputDecorationTheme: themeData.inputDecorationTheme.copyWith(
prefixIconColor: WidgetStateColor.fromMap(
prefixIconColor: const WidgetStateColor.fromMap(
<WidgetStatesConstraint, Color>{
WidgetState.error: Colors.red,
WidgetState.focused: Colors.blue,

View File

@ -45,7 +45,7 @@ class _ListTileExampleState extends State<ListTileExample> {
_selected = !_selected;
});
},
iconColor: WidgetStateColor.fromMap(<WidgetStatesConstraint, Color>{
iconColor: const WidgetStateColor.fromMap(<WidgetStatesConstraint, Color>{
WidgetState.disabled: Colors.red,
WidgetState.selected: Colors.green,
WidgetState.any: Colors.black,

View File

@ -246,6 +246,23 @@ abstract class ButtonStyleButton extends StatefulWidget {
/// A convenience method for subclasses.
static MaterialStateProperty<T>? allOrNull<T>(T? value) => value == null ? null : MaterialStatePropertyAll<T>(value);
/// Returns null if [enabled] and [disabled] are null.
/// Otherwise, returns a [WidgetStateProperty] that resolves to [disabled]
/// when [WidgetState.disabled] is active, and [enabled] otherwise.
///
/// A convenience method for subclasses.
static WidgetStateProperty<Color?>? defaultColor(Color? enabled, Color? disabled) {
if ((enabled ?? disabled) == null) {
return null;
}
return WidgetStateProperty<Color?>.fromMap(
<WidgetStatesConstraint, Color?>{
WidgetState.disabled: disabled,
WidgetState.any: enabled,
},
);
}
/// A convenience method used by subclasses in the framework, that returns an
/// interpolated value based on the [fontSizeMultiplier] parameter:
///

View File

@ -228,37 +228,39 @@ class ElevatedButton extends ButtonStyleButton {
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) {
final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
(null, null) => null,
(_, _) => _ElevatedButtonDefaultColor(foregroundColor, disabledForegroundColor),
};
final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
(null, null) => null,
(_, _) => _ElevatedButtonDefaultColor(backgroundColor, disabledBackgroundColor),
};
final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
(null, null) => null,
(_, _) => _ElevatedButtonDefaultColor(iconColor, disabledIconColor),
};
final MaterialStateProperty<Color?>? overlayColorProp = switch ((foregroundColor, overlayColor)) {
(null, null) => null,
(_, final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
(_, _) => _ElevatedButtonDefaultOverlay((overlayColor ?? foregroundColor)!),
(_, Color(a: 0.0)) => WidgetStatePropertyAll<Color?>(overlayColor),
(_, final Color color) || (final Color color, _) => WidgetStateProperty<Color?>.fromMap(
<WidgetState, Color?>{
WidgetState.pressed: color.withOpacity(0.1),
WidgetState.hovered: color.withOpacity(0.08),
WidgetState.focused: color.withOpacity(0.1),
},
),
};
final MaterialStateProperty<double>? elevationValue = switch (elevation) {
null => null,
_ => _ElevatedButtonDefaultElevation(elevation),
};
final MaterialStateProperty<MouseCursor?> mouseCursor = _ElevatedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
WidgetStateProperty<double>? elevationValue;
if (elevation != null) {
elevationValue = WidgetStateProperty<double>.fromMap(
<WidgetStatesConstraint, double>{
WidgetState.disabled: 0,
WidgetState.pressed: elevation + 6,
WidgetState.hovered: elevation + 2,
WidgetState.focused: elevation + 2,
WidgetState.any: elevation,
},
);
}
return ButtonStyle(
textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle),
backgroundColor: backgroundColorProp,
foregroundColor: foregroundColorProp,
backgroundColor: ButtonStyleButton.defaultColor(backgroundColor, disabledBackgroundColor),
foregroundColor: ButtonStyleButton.defaultColor(foregroundColor, disabledForegroundColor),
overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp,
iconColor: ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
elevation: elevationValue,
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
@ -266,7 +268,12 @@ class ElevatedButton extends ButtonStyleButton {
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
side: ButtonStyleButton.allOrNull<BorderSide>(side),
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
mouseCursor: mouseCursor,
mouseCursor: WidgetStateProperty<MouseCursor?>.fromMap(
<WidgetStatesConstraint, MouseCursor?>{
WidgetState.disabled: disabledMouseCursor,
WidgetState.any: enabledMouseCursor,
},
),
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
@ -453,83 +460,6 @@ EdgeInsetsGeometry _scaledPadding(BuildContext context) {
);
}
@immutable
class _ElevatedButtonDefaultColor extends MaterialStateProperty<Color?> with Diagnosticable {
_ElevatedButtonDefaultColor(this.color, this.disabled);
final Color? color;
final Color? disabled;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabled;
}
return color;
}
}
@immutable
class _ElevatedButtonDefaultOverlay extends MaterialStateProperty<Color?> with Diagnosticable {
_ElevatedButtonDefaultOverlay(this.overlay);
final Color overlay;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return overlay.withOpacity(0.1);
}
if (states.contains(MaterialState.hovered)) {
return overlay.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return overlay.withOpacity(0.1);
}
return null;
}
}
@immutable
class _ElevatedButtonDefaultElevation extends MaterialStateProperty<double> with Diagnosticable {
_ElevatedButtonDefaultElevation(this.elevation);
final double elevation;
@override
double resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return 0;
}
if (states.contains(MaterialState.pressed)) {
return elevation + 6;
}
if (states.contains(MaterialState.hovered)) {
return elevation + 2;
}
if (states.contains(MaterialState.focused)) {
return elevation + 2;
}
return elevation;
}
}
@immutable
class _ElevatedButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable {
_ElevatedButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
final MouseCursor? enabledCursor;
final MouseCursor? disabledCursor;
@override
MouseCursor? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledCursor;
}
return enabledCursor;
}
}
class _ElevatedButtonWithIcon extends ElevatedButton {
_ElevatedButtonWithIcon({
super.key,

View File

@ -291,33 +291,26 @@ class FilledButton extends ButtonStyleButton {
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) {
final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
(null, null) => null,
(_, _) => _FilledButtonDefaultColor(foregroundColor, disabledForegroundColor),
};
final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
(null, null) => null,
(_, _) => _FilledButtonDefaultColor(backgroundColor, disabledBackgroundColor),
};
final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
(null, null) => null,
(_, _) => _FilledButtonDefaultColor(iconColor, disabledIconColor),
};
final MaterialStateProperty<Color?>? overlayColorProp = switch ((foregroundColor, overlayColor)) {
(null, null) => null,
(_, final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
(_, _) => _FilledButtonDefaultOverlay((overlayColor ?? foregroundColor)!),
(_, Color(a: 0.0)) => WidgetStatePropertyAll<Color?>(overlayColor),
(_, final Color color) || (final Color color, _) => WidgetStateProperty<Color?>.fromMap(
<WidgetState, Color?>{
WidgetState.pressed: color.withOpacity(0.1),
WidgetState.hovered: color.withOpacity(0.08),
WidgetState.focused: color.withOpacity(0.1),
},
),
};
final MaterialStateProperty<MouseCursor?> mouseCursor = _FilledButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle(
textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle),
backgroundColor: backgroundColorProp,
foregroundColor: foregroundColorProp,
backgroundColor: ButtonStyleButton.defaultColor(backgroundColor, disabledBackgroundColor),
foregroundColor: ButtonStyleButton.defaultColor(foregroundColor, disabledForegroundColor),
overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp,
iconColor: ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
elevation: ButtonStyleButton.allOrNull(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
@ -325,7 +318,12 @@ class FilledButton extends ButtonStyleButton {
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
side: ButtonStyleButton.allOrNull<BorderSide>(side),
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
mouseCursor: mouseCursor,
mouseCursor: WidgetStateProperty<MouseCursor?>.fromMap(
<WidgetStatesConstraint, MouseCursor?>{
WidgetState.disabled: disabledMouseCursor,
WidgetState.any: enabledMouseCursor,
},
),
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
@ -483,59 +481,6 @@ EdgeInsetsGeometry _scaledPadding(BuildContext context) {
);
}
@immutable
class _FilledButtonDefaultColor extends MaterialStateProperty<Color?> with Diagnosticable {
_FilledButtonDefaultColor(this.color, this.disabled);
final Color? color;
final Color? disabled;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabled;
}
return color;
}
}
@immutable
class _FilledButtonDefaultOverlay extends MaterialStateProperty<Color?> with Diagnosticable {
_FilledButtonDefaultOverlay(this.overlay);
final Color overlay;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return overlay.withOpacity(0.1);
}
if (states.contains(MaterialState.hovered)) {
return overlay.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return overlay.withOpacity(0.1);
}
return null;
}
}
@immutable
class _FilledButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable {
_FilledButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
final MouseCursor? enabledCursor;
final MouseCursor? disabledCursor;
@override
MouseCursor? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledCursor;
}
return enabledCursor;
}
}
class _FilledButtonWithIcon extends FilledButton {
_FilledButtonWithIcon({
super.key,

View File

@ -643,24 +643,24 @@ class IconButton extends StatelessWidget {
AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory,
}) {
final MaterialStateProperty<Color?>? buttonBackgroundColor = (backgroundColor == null && disabledBackgroundColor == null)
? null
: _IconButtonDefaultBackground(backgroundColor, disabledBackgroundColor);
final MaterialStateProperty<Color?>? buttonForegroundColor = (foregroundColor == null && disabledForegroundColor == null)
? null
: _IconButtonDefaultForeground(foregroundColor, disabledForegroundColor);
final MaterialStateProperty<Color?>? overlayColorProp = (foregroundColor == null &&
hoverColor == null && focusColor == null && highlightColor == null && overlayColor == null)
? null
: switch (overlayColor) {
(final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
_ => _IconButtonDefaultOverlay(foregroundColor, focusColor, hoverColor, highlightColor, overlayColor),
};
final MaterialStateProperty<MouseCursor?> mouseCursor = _IconButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
final Color? overlayFallback = overlayColor ?? foregroundColor;
WidgetStateProperty<Color?>? overlayColorProp;
if ((hoverColor ?? focusColor ?? highlightColor ?? overlayFallback) != null) {
overlayColorProp = switch (overlayColor) {
Color(a: 0.0) => WidgetStatePropertyAll<Color>(overlayColor),
_ => WidgetStateProperty<Color?>.fromMap(
<WidgetState, Color?>{
WidgetState.pressed: highlightColor ?? overlayFallback?.withOpacity(0.1),
WidgetState.hovered: hoverColor ?? overlayFallback?.withOpacity(0.08),
WidgetState.focused: focusColor ?? overlayFallback?.withOpacity(0.1),
},
),
};
}
return ButtonStyle(
backgroundColor: buttonBackgroundColor,
foregroundColor: buttonForegroundColor,
backgroundColor: ButtonStyleButton.defaultColor(backgroundColor, disabledBackgroundColor),
foregroundColor: ButtonStyleButton.defaultColor(foregroundColor, disabledForegroundColor),
overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
@ -672,7 +672,12 @@ class IconButton extends StatelessWidget {
iconSize: ButtonStyleButton.allOrNull<double>(iconSize),
side: ButtonStyleButton.allOrNull<BorderSide>(side),
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
mouseCursor: mouseCursor,
mouseCursor: WidgetStateProperty<MouseCursor?>.fromMap(
<WidgetStatesConstraint, MouseCursor?>{
WidgetState.disabled: disabledMouseCursor,
WidgetState.any: enabledMouseCursor,
},
),
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
@ -993,111 +998,6 @@ class _IconButtonM3 extends ButtonStyleButton {
}
}
@immutable
class _IconButtonDefaultBackground extends MaterialStateProperty<Color?> {
_IconButtonDefaultBackground(this.background, this.disabledBackground);
final Color? background;
final Color? disabledBackground;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledBackground;
}
return background;
}
@override
String toString() {
return '{disabled: $disabledBackground, otherwise: $background}';
}
}
@immutable
class _IconButtonDefaultForeground extends MaterialStateProperty<Color?> {
_IconButtonDefaultForeground(this.foregroundColor, this.disabledForegroundColor);
final Color? foregroundColor;
final Color? disabledForegroundColor;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledForegroundColor;
}
return foregroundColor;
}
@override
String toString() {
return '{disabled: $disabledForegroundColor, otherwise: $foregroundColor}';
}
}
@immutable
class _IconButtonDefaultOverlay extends MaterialStateProperty<Color?> {
_IconButtonDefaultOverlay(
this.foregroundColor,
this.focusColor,
this.hoverColor,
this.highlightColor,
this.overlayColor,
);
final Color? foregroundColor;
final Color? focusColor;
final Color? hoverColor;
final Color? highlightColor;
final Color? overlayColor;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return highlightColor ?? (overlayColor ?? foregroundColor)?.withOpacity(0.1);
}
if (states.contains(MaterialState.hovered)) {
return hoverColor ?? (overlayColor ?? foregroundColor)?.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return focusColor ?? (overlayColor ?? foregroundColor)?.withOpacity(0.1);
}
}
if (states.contains(MaterialState.pressed)) {
return highlightColor ?? (overlayColor ?? foregroundColor)?.withOpacity(0.1);
}
if (states.contains(MaterialState.hovered)) {
return hoverColor ?? (overlayColor ?? foregroundColor)?.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return focusColor ?? (overlayColor ?? foregroundColor)?.withOpacity(0.1);
}
return null;
}
@override
String toString() {
return '{hovered: $hoverColor, focused: $focusColor, pressed: $highlightColor, otherwise: null}';
}
}
@immutable
class _IconButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable {
_IconButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
final MouseCursor? enabledCursor;
final MouseCursor? disabledCursor;
@override
MouseCursor? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledCursor;
}
return enabledCursor;
}
}
// BEGIN GENERATED TOKEN PROPERTIES - IconButton
// Do not edit by hand. The code between the "BEGIN GENERATED" and

View File

@ -215,34 +215,30 @@ class OutlinedButton extends ButtonStyleButton {
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) {
final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
(null, null) => null,
(_, _) => _OutlinedButtonDefaultColor(foregroundColor, disabledForegroundColor),
};
final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
(null, null) => null,
(_, null) => MaterialStatePropertyAll<Color?>(backgroundColor),
(_, _) => _OutlinedButtonDefaultColor(backgroundColor, disabledBackgroundColor),
};
final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
(null, null) => null,
(_, _) => _OutlinedButtonDefaultColor(iconColor, disabledIconColor),
(_?, null) => WidgetStatePropertyAll<Color?>(backgroundColor),
(_, _) => ButtonStyleButton.defaultColor(backgroundColor, disabledBackgroundColor),
};
final MaterialStateProperty<Color?>? overlayColorProp = switch ((foregroundColor, overlayColor)) {
(null, null) => null,
(_, final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
(_, _) => _OutlinedButtonDefaultOverlay((overlayColor ?? foregroundColor)!),
(_, Color(a: 0.0)) => WidgetStatePropertyAll<Color?>(overlayColor),
(_, final Color color) || (final Color color, _) => WidgetStateProperty<Color?>.fromMap(
<WidgetState, Color?>{
WidgetState.pressed: color.withOpacity(0.1),
WidgetState.hovered: color.withOpacity(0.08),
WidgetState.focused: color.withOpacity(0.1),
},
),
};
final MaterialStateProperty<MouseCursor?> mouseCursor = _OutlinedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle(
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
foregroundColor: foregroundColorProp,
foregroundColor: ButtonStyleButton.defaultColor(foregroundColor, disabledForegroundColor),
backgroundColor: backgroundColorProp,
overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp,
iconColor: ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
elevation: ButtonStyleButton.allOrNull<double>(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
@ -250,7 +246,12 @@ class OutlinedButton extends ButtonStyleButton {
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
side: ButtonStyleButton.allOrNull<BorderSide>(side),
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
mouseCursor: mouseCursor,
mouseCursor: WidgetStateProperty<MouseCursor?>.fromMap(
<WidgetStatesConstraint, MouseCursor?>{
WidgetState.disabled: disabledMouseCursor,
WidgetState.any: enabledMouseCursor,
},
),
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
@ -408,59 +409,6 @@ EdgeInsetsGeometry _scaledPadding(BuildContext context) {
);
}
@immutable
class _OutlinedButtonDefaultColor extends MaterialStateProperty<Color?> with Diagnosticable {
_OutlinedButtonDefaultColor(this.color, this.disabled);
final Color? color;
final Color? disabled;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabled;
}
return color;
}
}
@immutable
class _OutlinedButtonDefaultOverlay extends MaterialStateProperty<Color?> with Diagnosticable {
_OutlinedButtonDefaultOverlay(this.foreground);
final Color foreground;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return foreground.withOpacity(0.1);
}
if (states.contains(MaterialState.hovered)) {
return foreground.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return foreground.withOpacity(0.1);
}
return null;
}
}
@immutable
class _OutlinedButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable {
_OutlinedButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
final MouseCursor? enabledCursor;
final MouseCursor? disabledCursor;
@override
MouseCursor? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledCursor;
}
return enabledCursor;
}
}
class _OutlinedButtonWithIcon extends OutlinedButton {
_OutlinedButtonWithIcon({
super.key,

View File

@ -291,14 +291,6 @@ class SegmentedButton<T> extends StatefulWidget {
AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory,
}) {
final MaterialStateProperty<Color?>? foregroundColorProp =
(foregroundColor == null && disabledForegroundColor == null && selectedForegroundColor == null)
? null
: _SegmentButtonDefaultColor(foregroundColor, disabledForegroundColor, selectedForegroundColor);
final MaterialStateProperty<Color?>? backgroundColorProp =
(backgroundColor == null && disabledBackgroundColor == null && selectedBackgroundColor == null)
? null
: _SegmentButtonDefaultColor(backgroundColor, disabledBackgroundColor, selectedBackgroundColor);
final MaterialStateProperty<Color?>? overlayColorProp = (foregroundColor == null &&
selectedForegroundColor == null && overlayColor == null)
? null
@ -326,12 +318,25 @@ class SegmentedButton<T> extends StatefulWidget {
alignment: alignment,
splashFactory: splashFactory,
).copyWith(
foregroundColor: foregroundColorProp,
backgroundColor: backgroundColorProp,
foregroundColor: _defaultColor(foregroundColor, disabledForegroundColor, selectedForegroundColor),
backgroundColor: _defaultColor(backgroundColor, disabledBackgroundColor, selectedBackgroundColor),
overlayColor: overlayColorProp,
);
}
static WidgetStateProperty<Color?>? _defaultColor(Color? enabled, Color? disabled, Color? selected) {
if ((selected ?? enabled ?? disabled) == null) {
return null;
}
return WidgetStateProperty<Color?>.fromMap(
<WidgetStatesConstraint, Color?>{
WidgetState.disabled: disabled,
WidgetState.selected: selected,
WidgetState.any: enabled,
},
);
}
/// Customizes this button's appearance.
///
/// The following style properties apply to the entire segmented button:
@ -589,26 +594,6 @@ class SegmentedButtonState<T> extends State<SegmentedButton<T>> {
}
}
@immutable
class _SegmentButtonDefaultColor extends MaterialStateProperty<Color?> with Diagnosticable {
_SegmentButtonDefaultColor(this.color, this.disabled, this.selected);
final Color? color;
final Color? disabled;
final Color? selected;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabled;
}
if (states.contains(MaterialState.selected)) {
return selected;
}
return color;
}
}
class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
const _SegmentedButtonRenderWidget({
super.key,
@ -1081,31 +1066,24 @@ class _SegmentedButtonDefaultsM3 extends SegmentedButtonThemeData {
@override
Widget? get selectedIcon => const Icon(Icons.check);
static MaterialStateProperty<Color?> resolveStateColor(Color? unselectedColor, Color? selectedColor, Color? overlayColor){
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return (overlayColor ?? selectedColor)?.withOpacity(0.1);
}
if (states.contains(MaterialState.hovered)) {
return (overlayColor ?? selectedColor)?.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return (overlayColor ?? selectedColor)?.withOpacity(0.1);
}
} else {
if (states.contains(MaterialState.pressed)) {
return (overlayColor ?? unselectedColor)?.withOpacity(0.1);
}
if (states.contains(MaterialState.hovered)) {
return (overlayColor ?? unselectedColor)?.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return (overlayColor ?? unselectedColor)?.withOpacity(0.1);
}
}
return Colors.transparent;
});
static WidgetStateProperty<Color?> resolveStateColor(
Color? unselectedColor,
Color? selectedColor,
Color? overlayColor,
) {
final Color? selected = overlayColor ?? selectedColor;
final Color? unselected = overlayColor ?? unselectedColor;
return WidgetStateProperty<Color?>.fromMap(
<WidgetStatesConstraint, Color?>{
WidgetState.selected & WidgetState.pressed: selected?.withOpacity(0.1),
WidgetState.selected & WidgetState.hovered: selected?.withOpacity(0.08),
WidgetState.selected & WidgetState.focused: selected?.withOpacity(0.1),
WidgetState.pressed: unselected?.withOpacity(0.1),
WidgetState.hovered: unselected?.withOpacity(0.08),
WidgetState.focused: unselected?.withOpacity(0.1),
WidgetState.any: Colors.transparent,
},
);
}
}

View File

@ -222,30 +222,29 @@ class TextButton extends ButtonStyleButton {
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) {
final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
(null, null) => null,
(_, _) => _TextButtonDefaultColor(foregroundColor, disabledForegroundColor),
};
final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
(null, null) => null,
(_, null) => MaterialStatePropertyAll<Color?>(backgroundColor),
(_, _) => _TextButtonDefaultColor(backgroundColor, disabledBackgroundColor),
(_?, null) => MaterialStatePropertyAll<Color?>(backgroundColor),
(_, _) => ButtonStyleButton.defaultColor(backgroundColor, disabledBackgroundColor),
};
final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
(null, null) => null,
(_, null) => MaterialStatePropertyAll<Color?>(iconColor),
(_, _) => _TextButtonDefaultColor(iconColor, disabledIconColor),
(_?, null) => MaterialStatePropertyAll<Color?>(iconColor),
(_, _) => ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
};
final MaterialStateProperty<Color?>? overlayColorProp = switch ((foregroundColor, overlayColor)) {
(null, null) => null,
(_, final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
(_, _) => _TextButtonDefaultOverlay((overlayColor ?? foregroundColor)!),
(_, Color(a: 0.0)) => WidgetStatePropertyAll<Color?>(overlayColor),
(_, final Color color) || (final Color color, _) => WidgetStateProperty<Color?>.fromMap(
<WidgetState, Color?>{
WidgetState.pressed: color.withOpacity(0.1),
WidgetState.hovered: color.withOpacity(0.08),
WidgetState.focused: color.withOpacity(0.1),
},
),
};
final MaterialStateProperty<MouseCursor?> mouseCursor = _TextButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle(
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
foregroundColor: foregroundColorProp,
foregroundColor: ButtonStyleButton.defaultColor(foregroundColor, disabledForegroundColor),
backgroundColor: backgroundColorProp,
overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
@ -258,7 +257,12 @@ class TextButton extends ButtonStyleButton {
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
side: ButtonStyleButton.allOrNull<BorderSide>(side),
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
mouseCursor: mouseCursor,
mouseCursor: WidgetStateProperty<MouseCursor?>.fromMap(
<WidgetStatesConstraint, MouseCursor?>{
WidgetState.disabled: disabledMouseCursor,
WidgetState.any: enabledMouseCursor,
},
),
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
@ -434,69 +438,6 @@ EdgeInsetsGeometry _scaledPadding(BuildContext context) {
);
}
@immutable
class _TextButtonDefaultColor extends MaterialStateProperty<Color?> {
_TextButtonDefaultColor(this.color, this.disabled);
final Color? color;
final Color? disabled;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabled;
}
return color;
}
@override
String toString() {
return '{disabled: $disabled, otherwise: $color}';
}
}
@immutable
class _TextButtonDefaultOverlay extends MaterialStateProperty<Color?> {
_TextButtonDefaultOverlay(this.primary);
final Color primary;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return primary.withOpacity(0.1);
}
if (states.contains(MaterialState.hovered)) {
return primary.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return primary.withOpacity(0.1);
}
return null;
}
@override
String toString() {
return '{hovered: ${primary.withOpacity(0.04)}, focused,pressed: ${primary.withOpacity(0.12)}, otherwise: null}';
}
}
@immutable
class _TextButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable {
_TextButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
final MouseCursor? enabledCursor;
final MouseCursor? disabledCursor;
@override
MouseCursor? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledCursor;
}
return enabledCursor;
}
}
class _TextButtonWithIcon extends TextButton {
_TextButtonWithIcon({
super.key,

View File

@ -6,6 +6,7 @@
/// @docImport 'package:flutter/scheduler.dart';
library;
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
@ -45,14 +46,77 @@ abstract interface class WidgetStatesConstraint {
bool isSatisfiedBy(Set<WidgetState> states);
}
// A private class, used in [WidgetStateOperators].
class _WidgetStateOperation implements WidgetStatesConstraint {
const _WidgetStateOperation(this._isSatisfiedBy);
@immutable
sealed class _WidgetStateCombo implements WidgetStatesConstraint {
const _WidgetStateCombo(this.first, this.second);
final bool Function(Set<WidgetState> states) _isSatisfiedBy;
final WidgetStatesConstraint first;
final WidgetStatesConstraint second;
@override
bool isSatisfiedBy(Set<WidgetState> states) => _isSatisfiedBy(states);
// ignore: hash_and_equals, since == is defined in subclasses
int get hashCode => Object.hash(first, second);
}
class _WidgetStateAnd extends _WidgetStateCombo {
const _WidgetStateAnd(super.first, super.second);
@override
bool isSatisfiedBy(Set<WidgetState> states) {
return first.isSatisfiedBy(states) && second.isSatisfiedBy(states);
}
@override
// ignore: hash_and_equals, hashCode is defined in the sealed super-class
bool operator ==(Object other) {
return other is _WidgetStateAnd
&& other.first == first
&& other.second == second;
}
@override
String toString() => '($first & $second)';
}
class _WidgetStateOr extends _WidgetStateCombo {
const _WidgetStateOr(super.first, super.second);
@override
bool isSatisfiedBy(Set<WidgetState> states) {
return first.isSatisfiedBy(states) || second.isSatisfiedBy(states);
}
@override
// ignore: hash_and_equals, hashCode is defined in the sealed super-class
bool operator ==(Object other) {
return other is _WidgetStateOr
&& other.first == first
&& other.second == second;
}
@override
String toString() => '($first | $second)';
}
@immutable
class _WidgetStateNot implements WidgetStatesConstraint {
const _WidgetStateNot(this.value);
final WidgetStatesConstraint value;
@override
bool isSatisfiedBy(Set<WidgetState> states) => !value.isSatisfiedBy(states);
@override
bool operator ==(Object other) {
return other is _WidgetStateNot && other.value == value;
}
@override
int get hashCode => value.hashCode;
@override
String toString() => '~$value';
}
/// These operators can be used inside a [WidgetStateMap] to combine states
@ -67,33 +131,24 @@ class _WidgetStateOperation implements WidgetStatesConstraint {
/// the operators can be used without being directly inherited.
extension WidgetStateOperators on WidgetStatesConstraint {
/// Combines two [WidgetStatesConstraint] values using logical "and".
WidgetStatesConstraint operator &(WidgetStatesConstraint other) {
return _WidgetStateOperation(
(Set<WidgetState> states) => isSatisfiedBy(states) && other.isSatisfiedBy(states),
);
}
WidgetStatesConstraint operator &(WidgetStatesConstraint other) => _WidgetStateAnd(this, other);
/// Combines two [WidgetStatesConstraint] values using logical "or".
WidgetStatesConstraint operator |(WidgetStatesConstraint other) {
return _WidgetStateOperation(
(Set<WidgetState> states) => isSatisfiedBy(states) || other.isSatisfiedBy(states),
);
}
WidgetStatesConstraint operator |(WidgetStatesConstraint other) => _WidgetStateOr(this, other);
/// Takes a [WidgetStatesConstraint] and applies the logical "not".
WidgetStatesConstraint operator ~() {
return _WidgetStateOperation(
(Set<WidgetState> states) => !isSatisfiedBy(states),
);
}
WidgetStatesConstraint operator ~() => _WidgetStateNot(this);
}
// A private class, used to create [WidgetState.any].
class _AlwaysMatch implements WidgetStatesConstraint {
const _AlwaysMatch();
class _AnyWidgetStates implements WidgetStatesConstraint {
const _AnyWidgetStates();
@override
bool isSatisfiedBy(Set<WidgetState> states) => true;
@override
String toString() => 'WidgetState.any';
}
/// Interactive states that some of the widgets can take on when receiving input
@ -183,7 +238,7 @@ enum WidgetState implements WidgetStatesConstraint {
/// isn't satisfied by the given set of states, consier adding
/// [WidgetState.any] as the final [WidgetStateMap] key.
/// {@endtemplate}
static const WidgetStatesConstraint any = _AlwaysMatch();
static const WidgetStatesConstraint any = _AnyWidgetStates();
@override
bool isSatisfiedBy(Set<WidgetState> states) => states.contains(this);
@ -268,7 +323,7 @@ abstract class WidgetStateColor extends Color implements WidgetStateProperty<Col
/// [Set] of [WidgetState]s will be selected.
///
/// {@macro flutter.widgets.WidgetState.any}
factory WidgetStateColor.fromMap(WidgetStateMap<Color> map) = _WidgetStateColorMapper;
const factory WidgetStateColor.fromMap(WidgetStateMap<Color> map) = _WidgetStateColorMapper;
/// Returns a [Color] that's to be used when a component is in the specified
/// state.
@ -290,18 +345,6 @@ class _WidgetStateColor extends WidgetStateColor {
Color resolve(Set<WidgetState> states) => _resolve(states);
}
class _WidgetStateColorMapper extends WidgetStateColor {
_WidgetStateColorMapper(this.map)
: super(_WidgetStateMapper<Color>(map).resolve(_defaultStates).value);
final WidgetStateMap<Color> map;
static const Set<WidgetState> _defaultStates = <WidgetState>{};
@override
Color resolve(Set<WidgetState> states) => _WidgetStateMapper<Color>(map).resolve(states);
}
class _WidgetStateColorTransparent extends WidgetStateColor {
const _WidgetStateColorTransparent() : super(0x00000000);
@ -309,6 +352,26 @@ class _WidgetStateColorTransparent extends WidgetStateColor {
Color resolve(Set<WidgetState> states) => const Color(0x00000000);
}
@immutable
class _WidgetStateColorMapper extends _WidgetStateColorTransparent {
const _WidgetStateColorMapper(this.map);
final WidgetStateMap<Color> map;
_WidgetStateMapper<Color> get _mapper => _WidgetStateMapper<Color>(map);
@override
Color resolve(Set<WidgetState> states) => _mapper.resolve(states);
@override
bool operator ==(Object other) {
return other is _WidgetStateColorMapper && other.map == map;
}
@override
int get hashCode => map.hashCode;
}
/// Defines a [MouseCursor] whose value depends on a set of [WidgetState]s which
/// represent the interactive state of a component.
///
@ -548,8 +611,18 @@ class _WidgetBorderSideMapper extends WidgetStateBorderSide {
final WidgetStateMap<BorderSide?> map;
_WidgetStateMapper<BorderSide?> get _mapper => _WidgetStateMapper<BorderSide?>(map);
@override
BorderSide? resolve(Set<WidgetState> states) => _WidgetStateMapper<BorderSide?>(map).resolve(states);
BorderSide? resolve(Set<WidgetState> states) => _mapper.resolve(states);
@override
bool operator ==(Object other) {
return other is _WidgetBorderSideMapper && other.map == map;
}
@override
int get hashCode => map.hashCode;
}
/// Defines an [OutlinedBorder] whose value depends on a set of [WidgetState]s
@ -660,8 +733,18 @@ class _WidgetTextStyleMapper extends WidgetStateTextStyle {
final WidgetStateMap<TextStyle> map;
_WidgetStateMapper<TextStyle> get _mapper => _WidgetStateMapper<TextStyle>(map);
@override
TextStyle resolve(Set<WidgetState> states) => _WidgetStateMapper<TextStyle>(map).resolve(states);
TextStyle resolve(Set<WidgetState> states) => _mapper.resolve(states);
@override
bool operator ==(Object other) {
return other is _WidgetTextStyleMapper && other.map == map;
}
@override
int get hashCode => map.hashCode;
}
/// Interface for classes that [resolve] to a value of type `T` based
@ -847,6 +930,7 @@ class _WidgetStatePropertyWith<T> implements WidgetStateProperty<T> {
typedef WidgetStateMap<T> = Map<WidgetStatesConstraint, T>;
// A private class, used to create the [WidgetStateProperty.fromMap] constructor.
@immutable
class _WidgetStateMapper<T> implements WidgetStateProperty<T> {
const _WidgetStateMapper(this.map);
@ -872,6 +956,17 @@ class _WidgetStateMapper<T> implements WidgetStateProperty<T> {
);
}
}
@override
bool operator ==(Object other) {
return other is _WidgetStateMapper && mapEquals(map, other.map);
}
@override
int get hashCode => MapEquality<WidgetStatesConstraint, T>().hash(map);
@override
String toString() => '$map';
}
/// Convenience class for creating a [WidgetStateProperty] that
@ -881,6 +976,7 @@ class _WidgetStateMapper<T> implements WidgetStateProperty<T> {
///
/// * [MaterialStatePropertyAll], the Material specific version of
/// `WidgetStatePropertyAll`.
@immutable
class WidgetStatePropertyAll<T> implements WidgetStateProperty<T> {
/// Constructs a [WidgetStateProperty] that always resolves to the given
@ -901,6 +997,16 @@ class WidgetStatePropertyAll<T> implements WidgetStateProperty<T> {
return 'WidgetStatePropertyAll($value)';
}
}
@override
bool operator ==(Object other) {
return other is WidgetStatePropertyAll<T>
&& other.runtimeType == runtimeType
&& other.value == value;
}
@override
int get hashCode => value.hashCode;
}
/// Manages a set of [WidgetState]s and notifies listeners of changes.

View File

@ -21,6 +21,44 @@ void main() {
expect(dawn.primaryColor, Color.lerp(dark.primaryColor, light.primaryColor, 0.25));
});
test('ThemeData objects with .styleFrom() members are equal', () {
ThemeData createThemeData() {
return ThemeData(
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.black,
backgroundColor: Colors.black,
elevation: 1.0,
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
foregroundColor: Colors.black,
disabledForegroundColor: Colors.black,
backgroundColor: Colors.black,
disabledBackgroundColor: Colors.black,
overlayColor: Colors.black,
),
),
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
hoverColor: Colors.black,
focusColor: Colors.black,
highlightColor: Colors.black,
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
enabledMouseCursor: MouseCursor.defer,
disabledMouseCursor: MouseCursor.uncontrolled,
),
),
);
}
expect(createThemeData() == createThemeData(), isTrue);
});
test('Defaults to the default typography for the platform', () {
for (final TargetPlatform platform in TargetPlatform.values) {
final ThemeData theme = ThemeData(platform: platform, useMaterial3: false);