Skip to content

Commit bfa04ed

Browse files
un-break ThemeData equality (flutter#154695)
This PR is _almost_ able to close issue flutter#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: flutter#89127 (comment)
1 parent 18c325a commit bfa04ed

File tree

13 files changed

+357
-561
lines changed

13 files changed

+357
-561
lines changed

dev/tools/gen_defaults/lib/segmented_button_template.dart

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -120,31 +120,24 @@ class _${blockName}DefaultsM3 extends SegmentedButtonThemeData {
120120
@override
121121
Widget? get selectedIcon => const Icon(Icons.check);
122122
123-
static MaterialStateProperty<Color?> resolveStateColor(Color? unselectedColor, Color? selectedColor, Color? overlayColor){
124-
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
125-
if (states.contains(MaterialState.selected)) {
126-
if (states.contains(MaterialState.pressed)) {
127-
return (overlayColor ?? selectedColor)?.withOpacity(0.1);
128-
}
129-
if (states.contains(MaterialState.hovered)) {
130-
return (overlayColor ?? selectedColor)?.withOpacity(0.08);
131-
}
132-
if (states.contains(MaterialState.focused)) {
133-
return (overlayColor ?? selectedColor)?.withOpacity(0.1);
134-
}
135-
} else {
136-
if (states.contains(MaterialState.pressed)) {
137-
return (overlayColor ?? unselectedColor)?.withOpacity(0.1);
138-
}
139-
if (states.contains(MaterialState.hovered)) {
140-
return (overlayColor ?? unselectedColor)?.withOpacity(0.08);
141-
}
142-
if (states.contains(MaterialState.focused)) {
143-
return (overlayColor ?? unselectedColor)?.withOpacity(0.1);
144-
}
145-
}
146-
return Colors.transparent;
147-
});
123+
static WidgetStateProperty<Color?> resolveStateColor(
124+
Color? unselectedColor,
125+
Color? selectedColor,
126+
Color? overlayColor,
127+
) {
128+
final Color? selected = overlayColor ?? selectedColor;
129+
final Color? unselected = overlayColor ?? unselectedColor;
130+
return WidgetStateProperty<Color?>.fromMap(
131+
<WidgetStatesConstraint, Color?>{
132+
WidgetState.selected & WidgetState.pressed: selected?.withOpacity(0.1),
133+
WidgetState.selected & WidgetState.hovered: selected?.withOpacity(0.08),
134+
WidgetState.selected & WidgetState.focused: selected?.withOpacity(0.1),
135+
WidgetState.pressed: unselected?.withOpacity(0.1),
136+
WidgetState.hovered: unselected?.withOpacity(0.08),
137+
WidgetState.focused: unselected?.withOpacity(0.1),
138+
WidgetState.any: Colors.transparent,
139+
},
140+
);
148141
}
149142
}
150143
''';

examples/api/lib/material/input_decorator/input_decoration.widget_state.0.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ class MaterialStateExample extends StatelessWidget {
3030
Widget build(BuildContext context) {
3131
return TextFormField(
3232
initialValue: 'abc',
33-
decoration: InputDecoration(
34-
prefixIcon: const Icon(Icons.person),
33+
decoration: const InputDecoration(
34+
prefixIcon: Icon(Icons.person),
3535
prefixIconColor: WidgetStateColor.fromMap(
3636
<WidgetStatesConstraint, Color>{
3737
WidgetState.focused: Colors.green,

examples/api/lib/material/input_decorator/input_decoration.widget_state.1.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class MaterialStateExample extends StatelessWidget {
3232
return Theme(
3333
data: themeData.copyWith(
3434
inputDecorationTheme: themeData.inputDecorationTheme.copyWith(
35-
prefixIconColor: WidgetStateColor.fromMap(
35+
prefixIconColor: const WidgetStateColor.fromMap(
3636
<WidgetStatesConstraint, Color>{
3737
WidgetState.error: Colors.red,
3838
WidgetState.focused: Colors.blue,

examples/api/lib/material/list_tile/list_tile.3.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class _ListTileExampleState extends State<ListTileExample> {
4545
_selected = !_selected;
4646
});
4747
},
48-
iconColor: WidgetStateColor.fromMap(<WidgetStatesConstraint, Color>{
48+
iconColor: const WidgetStateColor.fromMap(<WidgetStatesConstraint, Color>{
4949
WidgetState.disabled: Colors.red,
5050
WidgetState.selected: Colors.green,
5151
WidgetState.any: Colors.black,

packages/flutter/lib/src/material/button_style_button.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,23 @@ abstract class ButtonStyleButton extends StatefulWidget {
246246
/// A convenience method for subclasses.
247247
static MaterialStateProperty<T>? allOrNull<T>(T? value) => value == null ? null : MaterialStatePropertyAll<T>(value);
248248

249+
/// Returns null if [enabled] and [disabled] are null.
250+
/// Otherwise, returns a [WidgetStateProperty] that resolves to [disabled]
251+
/// when [WidgetState.disabled] is active, and [enabled] otherwise.
252+
///
253+
/// A convenience method for subclasses.
254+
static WidgetStateProperty<Color?>? defaultColor(Color? enabled, Color? disabled) {
255+
if ((enabled ?? disabled) == null) {
256+
return null;
257+
}
258+
return WidgetStateProperty<Color?>.fromMap(
259+
<WidgetStatesConstraint, Color?>{
260+
WidgetState.disabled: disabled,
261+
WidgetState.any: enabled,
262+
},
263+
);
264+
}
265+
249266
/// A convenience method used by subclasses in the framework, that returns an
250267
/// interpolated value based on the [fontSizeMultiplier] parameter:
251268
///

packages/flutter/lib/src/material/elevated_button.dart

Lines changed: 30 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -228,45 +228,52 @@ class ElevatedButton extends ButtonStyleButton {
228228
ButtonLayerBuilder? backgroundBuilder,
229229
ButtonLayerBuilder? foregroundBuilder,
230230
}) {
231-
final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
232-
(null, null) => null,
233-
(_, _) => _ElevatedButtonDefaultColor(foregroundColor, disabledForegroundColor),
234-
};
235-
final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
236-
(null, null) => null,
237-
(_, _) => _ElevatedButtonDefaultColor(backgroundColor, disabledBackgroundColor),
238-
};
239-
final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
240-
(null, null) => null,
241-
(_, _) => _ElevatedButtonDefaultColor(iconColor, disabledIconColor),
242-
};
243231
final MaterialStateProperty<Color?>? overlayColorProp = switch ((foregroundColor, overlayColor)) {
244232
(null, null) => null,
245-
(_, final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
246-
(_, _) => _ElevatedButtonDefaultOverlay((overlayColor ?? foregroundColor)!),
233+
(_, Color(a: 0.0)) => WidgetStatePropertyAll<Color?>(overlayColor),
234+
(_, final Color color) || (final Color color, _) => WidgetStateProperty<Color?>.fromMap(
235+
<WidgetState, Color?>{
236+
WidgetState.pressed: color.withOpacity(0.1),
237+
WidgetState.hovered: color.withOpacity(0.08),
238+
WidgetState.focused: color.withOpacity(0.1),
239+
},
240+
),
247241
};
248-
final MaterialStateProperty<double>? elevationValue = switch (elevation) {
249-
null => null,
250-
_ => _ElevatedButtonDefaultElevation(elevation),
251-
};
252-
final MaterialStateProperty<MouseCursor?> mouseCursor = _ElevatedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
242+
243+
WidgetStateProperty<double>? elevationValue;
244+
if (elevation != null) {
245+
elevationValue = WidgetStateProperty<double>.fromMap(
246+
<WidgetStatesConstraint, double>{
247+
WidgetState.disabled: 0,
248+
WidgetState.pressed: elevation + 6,
249+
WidgetState.hovered: elevation + 2,
250+
WidgetState.focused: elevation + 2,
251+
WidgetState.any: elevation,
252+
},
253+
);
254+
}
253255

254256
return ButtonStyle(
255257
textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle),
256-
backgroundColor: backgroundColorProp,
257-
foregroundColor: foregroundColorProp,
258+
backgroundColor: ButtonStyleButton.defaultColor(backgroundColor, disabledBackgroundColor),
259+
foregroundColor: ButtonStyleButton.defaultColor(foregroundColor, disabledForegroundColor),
258260
overlayColor: overlayColorProp,
259261
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
260262
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
261-
iconColor: iconColorProp,
263+
iconColor: ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
262264
elevation: elevationValue,
263265
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
264266
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
265267
fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
266268
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
267269
side: ButtonStyleButton.allOrNull<BorderSide>(side),
268270
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
269-
mouseCursor: mouseCursor,
271+
mouseCursor: WidgetStateProperty<MouseCursor?>.fromMap(
272+
<WidgetStatesConstraint, MouseCursor?>{
273+
WidgetState.disabled: disabledMouseCursor,
274+
WidgetState.any: enabledMouseCursor,
275+
},
276+
),
270277
visualDensity: visualDensity,
271278
tapTargetSize: tapTargetSize,
272279
animationDuration: animationDuration,
@@ -453,83 +460,6 @@ EdgeInsetsGeometry _scaledPadding(BuildContext context) {
453460
);
454461
}
455462

456-
@immutable
457-
class _ElevatedButtonDefaultColor extends MaterialStateProperty<Color?> with Diagnosticable {
458-
_ElevatedButtonDefaultColor(this.color, this.disabled);
459-
460-
final Color? color;
461-
final Color? disabled;
462-
463-
@override
464-
Color? resolve(Set<MaterialState> states) {
465-
if (states.contains(MaterialState.disabled)) {
466-
return disabled;
467-
}
468-
return color;
469-
}
470-
}
471-
472-
@immutable
473-
class _ElevatedButtonDefaultOverlay extends MaterialStateProperty<Color?> with Diagnosticable {
474-
_ElevatedButtonDefaultOverlay(this.overlay);
475-
476-
final Color overlay;
477-
478-
@override
479-
Color? resolve(Set<MaterialState> states) {
480-
if (states.contains(MaterialState.pressed)) {
481-
return overlay.withOpacity(0.1);
482-
}
483-
if (states.contains(MaterialState.hovered)) {
484-
return overlay.withOpacity(0.08);
485-
}
486-
if (states.contains(MaterialState.focused)) {
487-
return overlay.withOpacity(0.1);
488-
}
489-
return null;
490-
}
491-
}
492-
493-
@immutable
494-
class _ElevatedButtonDefaultElevation extends MaterialStateProperty<double> with Diagnosticable {
495-
_ElevatedButtonDefaultElevation(this.elevation);
496-
497-
final double elevation;
498-
499-
@override
500-
double resolve(Set<MaterialState> states) {
501-
if (states.contains(MaterialState.disabled)) {
502-
return 0;
503-
}
504-
if (states.contains(MaterialState.pressed)) {
505-
return elevation + 6;
506-
}
507-
if (states.contains(MaterialState.hovered)) {
508-
return elevation + 2;
509-
}
510-
if (states.contains(MaterialState.focused)) {
511-
return elevation + 2;
512-
}
513-
return elevation;
514-
}
515-
}
516-
517-
@immutable
518-
class _ElevatedButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable {
519-
_ElevatedButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
520-
521-
final MouseCursor? enabledCursor;
522-
final MouseCursor? disabledCursor;
523-
524-
@override
525-
MouseCursor? resolve(Set<MaterialState> states) {
526-
if (states.contains(MaterialState.disabled)) {
527-
return disabledCursor;
528-
}
529-
return enabledCursor;
530-
}
531-
}
532-
533463
class _ElevatedButtonWithIcon extends ElevatedButton {
534464
_ElevatedButtonWithIcon({
535465
super.key,

packages/flutter/lib/src/material/filled_button.dart

Lines changed: 17 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -291,41 +291,39 @@ class FilledButton extends ButtonStyleButton {
291291
ButtonLayerBuilder? backgroundBuilder,
292292
ButtonLayerBuilder? foregroundBuilder,
293293
}) {
294-
final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
295-
(null, null) => null,
296-
(_, _) => _FilledButtonDefaultColor(foregroundColor, disabledForegroundColor),
297-
};
298-
final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
299-
(null, null) => null,
300-
(_, _) => _FilledButtonDefaultColor(backgroundColor, disabledBackgroundColor),
301-
};
302-
final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
303-
(null, null) => null,
304-
(_, _) => _FilledButtonDefaultColor(iconColor, disabledIconColor),
305-
};
306294
final MaterialStateProperty<Color?>? overlayColorProp = switch ((foregroundColor, overlayColor)) {
307295
(null, null) => null,
308-
(_, final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
309-
(_, _) => _FilledButtonDefaultOverlay((overlayColor ?? foregroundColor)!),
296+
(_, Color(a: 0.0)) => WidgetStatePropertyAll<Color?>(overlayColor),
297+
(_, final Color color) || (final Color color, _) => WidgetStateProperty<Color?>.fromMap(
298+
<WidgetState, Color?>{
299+
WidgetState.pressed: color.withOpacity(0.1),
300+
WidgetState.hovered: color.withOpacity(0.08),
301+
WidgetState.focused: color.withOpacity(0.1),
302+
},
303+
),
310304
};
311-
final MaterialStateProperty<MouseCursor?> mouseCursor = _FilledButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
312305

313306
return ButtonStyle(
314307
textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle),
315-
backgroundColor: backgroundColorProp,
316-
foregroundColor: foregroundColorProp,
308+
backgroundColor: ButtonStyleButton.defaultColor(backgroundColor, disabledBackgroundColor),
309+
foregroundColor: ButtonStyleButton.defaultColor(foregroundColor, disabledForegroundColor),
317310
overlayColor: overlayColorProp,
318311
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
319312
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
320-
iconColor: iconColorProp,
313+
iconColor: ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
321314
elevation: ButtonStyleButton.allOrNull(elevation),
322315
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
323316
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
324317
fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
325318
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
326319
side: ButtonStyleButton.allOrNull<BorderSide>(side),
327320
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
328-
mouseCursor: mouseCursor,
321+
mouseCursor: WidgetStateProperty<MouseCursor?>.fromMap(
322+
<WidgetStatesConstraint, MouseCursor?>{
323+
WidgetState.disabled: disabledMouseCursor,
324+
WidgetState.any: enabledMouseCursor,
325+
},
326+
),
329327
visualDensity: visualDensity,
330328
tapTargetSize: tapTargetSize,
331329
animationDuration: animationDuration,
@@ -483,59 +481,6 @@ EdgeInsetsGeometry _scaledPadding(BuildContext context) {
483481
);
484482
}
485483

486-
@immutable
487-
class _FilledButtonDefaultColor extends MaterialStateProperty<Color?> with Diagnosticable {
488-
_FilledButtonDefaultColor(this.color, this.disabled);
489-
490-
final Color? color;
491-
final Color? disabled;
492-
493-
@override
494-
Color? resolve(Set<MaterialState> states) {
495-
if (states.contains(MaterialState.disabled)) {
496-
return disabled;
497-
}
498-
return color;
499-
}
500-
}
501-
502-
@immutable
503-
class _FilledButtonDefaultOverlay extends MaterialStateProperty<Color?> with Diagnosticable {
504-
_FilledButtonDefaultOverlay(this.overlay);
505-
506-
final Color overlay;
507-
508-
@override
509-
Color? resolve(Set<MaterialState> states) {
510-
if (states.contains(MaterialState.pressed)) {
511-
return overlay.withOpacity(0.1);
512-
}
513-
if (states.contains(MaterialState.hovered)) {
514-
return overlay.withOpacity(0.08);
515-
}
516-
if (states.contains(MaterialState.focused)) {
517-
return overlay.withOpacity(0.1);
518-
}
519-
return null;
520-
}
521-
}
522-
523-
@immutable
524-
class _FilledButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable {
525-
_FilledButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
526-
527-
final MouseCursor? enabledCursor;
528-
final MouseCursor? disabledCursor;
529-
530-
@override
531-
MouseCursor? resolve(Set<MaterialState> states) {
532-
if (states.contains(MaterialState.disabled)) {
533-
return disabledCursor;
534-
}
535-
return enabledCursor;
536-
}
537-
}
538-
539484
class _FilledButtonWithIcon extends FilledButton {
540485
_FilledButtonWithIcon({
541486
super.key,

0 commit comments

Comments
 (0)