@@ -8,6 +8,89 @@ import 'package:flutter/services.dart';
88
99// Examples can assume:
1010// late BuildContext context;
11+ // late Set<WidgetState> states;
12+
13+ /// This class allows [WidgetState] enum values to be combined
14+ /// using [WidgetStateOperators] .
15+ ///
16+ /// A [Map] with [WidgetStatesConstraint] objects as keys can be used
17+ /// in the [WidgetStateProperty.fromMap] constructor to resolve to
18+ /// one of its values, based on the first key that [isSatisfiedBy]
19+ /// the current set of states.
20+ ///
21+ /// {@macro flutter.widgets.WidgetStateMap}
22+ abstract interface class WidgetStatesConstraint {
23+ /// Whether the provided [states] satisfy this object's criteria.
24+ ///
25+ /// If the constraint is a single [WidgetState] object,
26+ /// it's satisfied by the set if the set contains the object.
27+ ///
28+ /// The constraint can also be created using one or more operators, for example:
29+ ///
30+ /// {@template flutter.widgets.WidgetStatesConstraint.isSatisfiedBy}
31+ /// ```dart
32+ /// final WidgetStatesConstraint constraint = WidgetState.focused | WidgetState.hovered;
33+ /// ```
34+ ///
35+ /// In the above case, `constraint.isSatisfiedBy(states)` is equivalent to:
36+ ///
37+ /// ```dart
38+ /// states.contains(WidgetState.focused) || states.contains(WidgetState.hovered);
39+ /// ```
40+ /// {@endtemplate}
41+ bool isSatisfiedBy (Set <WidgetState > states);
42+ }
43+
44+ // A private class, used in [WidgetStateOperators].
45+ class _WidgetStateOperation implements WidgetStatesConstraint {
46+ const _WidgetStateOperation (this ._isSatisfiedBy);
47+
48+ final bool Function (Set <WidgetState > states) _isSatisfiedBy;
49+
50+ @override
51+ bool isSatisfiedBy (Set <WidgetState > states) => _isSatisfiedBy (states);
52+ }
53+
54+ /// These operators can be used inside a [WidgetStateMap] to combine states
55+ /// and find a match.
56+ ///
57+ /// Example:
58+ ///
59+ /// {@macro flutter.widgets.WidgetStatesConstraint.isSatisfiedBy}
60+ ///
61+ /// Since enums can't extend other classes, [WidgetState] instead `implements`
62+ /// the [WidgetStatesConstraint] interface. This `extension` ensures that
63+ /// the operators can be used without being directly inherited.
64+ extension WidgetStateOperators on WidgetStatesConstraint {
65+ /// Combines two [WidgetStatesConstraint] values using logical "and".
66+ WidgetStatesConstraint operator & (WidgetStatesConstraint other) {
67+ return _WidgetStateOperation (
68+ (Set <WidgetState > states) => isSatisfiedBy (states) && other.isSatisfiedBy (states),
69+ );
70+ }
71+
72+ /// Combines two [WidgetStatesConstraint] values using logical "or".
73+ WidgetStatesConstraint operator | (WidgetStatesConstraint other) {
74+ return _WidgetStateOperation (
75+ (Set <WidgetState > states) => isSatisfiedBy (states) || other.isSatisfiedBy (states),
76+ );
77+ }
78+
79+ /// Takes a [WidgetStatesConstraint] and applies the logical "not".
80+ WidgetStatesConstraint operator ~ () {
81+ return _WidgetStateOperation (
82+ (Set <WidgetState > states) => ! isSatisfiedBy (states),
83+ );
84+ }
85+ }
86+
87+ // A private class, used to create [WidgetState.any].
88+ class _AlwaysMatch implements WidgetStatesConstraint {
89+ const _AlwaysMatch ();
90+
91+ @override
92+ bool isSatisfiedBy (Set <WidgetState > states) => true ;
93+ }
1194
1295/// Interactive states that some of the widgets can take on when receiving input
1396/// from the user.
@@ -39,7 +122,7 @@ import 'package:flutter/services.dart';
39122/// `WidgetStateProperty` which is used in APIs that need to accept either
40123/// a [TextStyle] or a [WidgetStateProperty<TextStyle>].
41124/// {@endtemplate}
42- enum WidgetState {
125+ enum WidgetState implements WidgetStatesConstraint {
43126 /// The state when the user drags their mouse cursor over the given widget.
44127 ///
45128 /// See: https://material.io/design/interaction/states.html#hover.
@@ -89,7 +172,17 @@ enum WidgetState {
89172 /// The state when the widget has entered some form of invalid state.
90173 ///
91174 /// See https://material.io/design/interaction/states.html#usage.
92- error,
175+ error;
176+
177+ /// {@template flutter.widgets.WidgetState.any}
178+ /// To prevent a situation where each [WidgetStatesConstraint]
179+ /// isn't satisfied by the given set of states, consier adding
180+ /// [WidgetState.any] as the final [WidgetStateMap] key.
181+ /// {@endtemplate}
182+ static const WidgetStatesConstraint any = _AlwaysMatch ();
183+
184+ @override
185+ bool isSatisfiedBy (Set <WidgetState > states) => states.contains (this );
93186}
94187
95188/// Signature for the function that returns a value of type `T` based on a given
@@ -112,6 +205,7 @@ typedef WidgetPropertyResolver<T> = T Function(Set<WidgetState> states);
112205/// 1. Create a subclass of [WidgetStateColor] and implement the abstract `resolve` method.
113206/// 2. Use [WidgetStateColor.resolveWith] and pass in a callback that
114207/// will be used to resolve the color in the given states.
208+ /// 3. Use [WidgetStateColor.fromMap] to assign a value using a [WidgetStateMap] .
115209///
116210/// If a [WidgetStateColor] is used for a property or a parameter that doesn't
117211/// support resolving [WidgetStateProperty<Color>] s, then its default color
@@ -160,7 +254,17 @@ abstract class WidgetStateColor extends Color implements WidgetStateProperty<Col
160254 ///
161255 /// The given callback parameter must return a non-null color in the default
162256 /// state.
163- static WidgetStateColor resolveWith (WidgetPropertyResolver <Color > callback) => _WidgetStateColor (callback);
257+ factory WidgetStateColor .resolveWith (WidgetPropertyResolver <Color > callback) = _WidgetStateColor ;
258+
259+ /// Creates a [WidgetStateColor] from a [WidgetStateMap<Color>] .
260+ ///
261+ /// {@macro flutter.widgets.WidgetStateProperty.fromMap}
262+ ///
263+ /// If used as a regular color, the first key that matches an empty
264+ /// [Set] of [WidgetState] s will be selected.
265+ ///
266+ /// {@macro flutter.widgets.WidgetState.any}
267+ factory WidgetStateColor .fromMap (WidgetStateMap <Color > map) = _WidgetStateColorMapper ;
164268
165269 /// Returns a [Color] that's to be used when a component is in the specified
166270 /// state.
@@ -182,6 +286,18 @@ class _WidgetStateColor extends WidgetStateColor {
182286 Color resolve (Set <WidgetState > states) => _resolve (states);
183287}
184288
289+ class _WidgetStateColorMapper extends WidgetStateColor {
290+ _WidgetStateColorMapper (this .map)
291+ : super (_WidgetStateMapper <Color >(map).resolve (_defaultStates).value);
292+
293+ final WidgetStateMap <Color > map;
294+
295+ static const Set <WidgetState > _defaultStates = < WidgetState > {};
296+
297+ @override
298+ Color resolve (Set <WidgetState > states) => _WidgetStateMapper <Color >(map).resolve (states);
299+ }
300+
185301class _WidgetStateColorTransparent extends WidgetStateColor {
186302 const _WidgetStateColorTransparent () : super (0x00000000 );
187303
@@ -348,8 +464,30 @@ abstract class WidgetStateBorderSide extends BorderSide implements WidgetStatePr
348464 /// ```
349465 const factory WidgetStateBorderSide .resolveWith (WidgetPropertyResolver <BorderSide ?> callback) = _WidgetStateBorderSide ;
350466
351- /// Returns a [BorderSide] that's to be used when a Material component is
352- /// in the specified state. Return null to defer to the default value of the
467+ /// Creates a [WidgetStateBorderSide] from a [WidgetStateMap] .
468+ ///
469+ /// {@macro flutter.widgets.WidgetStateProperty.fromMap}
470+ ///
471+ /// If used as a regular [BorderSide] , the first key that matches an empty
472+ /// [Set] of [WidgetState] s will be selected.
473+ ///
474+ /// Example:
475+ ///
476+ /// ```dart
477+ /// const Chip(
478+ /// label: Text('Transceiver'),
479+ /// side: WidgetStateBorderSide.fromMap(<WidgetStatesConstraint, BorderSide?>{
480+ /// WidgetState.selected: BorderSide(color: Colors.red),
481+ /// // returns null if not selected, deferring to default theme/widget value.
482+ /// }),
483+ /// ),
484+ /// ```
485+ ///
486+ /// {@macro flutter.widgets.WidgetState.any}
487+ const factory WidgetStateBorderSide .fromMap (WidgetStateMap <BorderSide ?> map) = _WidgetBorderSideMapper ;
488+
489+ /// Returns a [BorderSide] that's to be used when a Widget is in the
490+ /// specified state. Return null to defer to the default value of the
353491 /// widget or theme.
354492 @override
355493 BorderSide ? resolve (Set <WidgetState > states);
@@ -401,6 +539,15 @@ class _WidgetStateBorderSide extends WidgetStateBorderSide {
401539 BorderSide ? resolve (Set <WidgetState > states) => _resolve (states);
402540}
403541
542+ class _WidgetBorderSideMapper extends WidgetStateBorderSide {
543+ const _WidgetBorderSideMapper (this .map);
544+
545+ final WidgetStateMap <BorderSide ?> map;
546+
547+ @override
548+ BorderSide ? resolve (Set <WidgetState > states) => _WidgetStateMapper <BorderSide ?>(map).resolve (states);
549+ }
550+
404551/// Defines an [OutlinedBorder] whose value depends on a set of [WidgetState] s
405552/// which represent the interactive state of a component.
406553///
@@ -452,6 +599,7 @@ abstract class WidgetStateOutlinedBorder extends OutlinedBorder implements Widge
452599/// 1. Create a subclass of [WidgetStateTextStyle] and implement the abstract `resolve` method.
453600/// 2. Use [WidgetStateTextStyle.resolveWith] and pass in a callback that
454601/// will be used to resolve the color in the given states.
602+ /// 3. Use [WidgetStateTextStyle.fromMap] to assign a style using a [WidgetStateMap] .
455603///
456604/// If a [WidgetStateTextStyle] is used for a property or a parameter that doesn't
457605/// support resolving [WidgetStateProperty<TextStyle>] s, then its default color
@@ -480,6 +628,16 @@ abstract class WidgetStateTextStyle extends TextStyle implements WidgetStateProp
480628 /// state.
481629 const factory WidgetStateTextStyle .resolveWith (WidgetPropertyResolver <TextStyle > callback) = _WidgetStateTextStyle ;
482630
631+ /// Creates a [WidgetStateTextStyle] from a [WidgetStateMap] .
632+ ///
633+ /// {@macro flutter.widgets.WidgetStateProperty.fromMap}
634+ ///
635+ /// If used as a regular text style, the first key that matches an empty
636+ /// [Set] of [WidgetState] s will be selected.
637+ ///
638+ /// {@macro flutter.widgets.WidgetState.any}
639+ const factory WidgetStateTextStyle .fromMap (WidgetStateMap <TextStyle > map) = _WidgetTextStyleMapper ;
640+
483641 /// Returns a [TextStyle] that's to be used when a component is in the
484642 /// specified state.
485643 @override
@@ -495,6 +653,15 @@ class _WidgetStateTextStyle extends WidgetStateTextStyle {
495653 TextStyle resolve (Set <WidgetState > states) => _resolve (states);
496654}
497655
656+ class _WidgetTextStyleMapper extends WidgetStateTextStyle {
657+ const _WidgetTextStyleMapper (this .map);
658+
659+ final WidgetStateMap <TextStyle > map;
660+
661+ @override
662+ TextStyle resolve (Set <WidgetState > states) => _WidgetStateMapper <TextStyle >(map).resolve (states);
663+ }
664+
498665/// Interface for classes that [resolve] to a value of type `T` based
499666/// on a widget's interactive "state", which is defined as a set
500667/// of [WidgetState] s.
@@ -519,12 +686,26 @@ class _WidgetStateTextStyle extends WidgetStateTextStyle {
519686/// `WidgetStateProperty`.
520687/// {@macro flutter.widgets.WidgetStateProperty.implementations}
521688abstract class WidgetStateProperty <T > {
522- /// Returns a value of type `T` that depends on [states] .
689+ /// This abstract constructor allows extending the class .
523690 ///
524- /// Widgets like [TextButton] and [ElevatedButton] apply this method to their
525- /// current [WidgetState] s to compute colors and other visual parameters
526- /// at build time.
527- T resolve (Set <WidgetState > states);
691+ /// [WidgetStateProperty] is designed as an interface, so this constructor
692+ /// is only needed for backward compatibility.
693+ WidgetStateProperty ();
694+
695+ /// Creates a property that resolves using a [WidgetStateMap] .
696+ ///
697+ /// {@template flutter.widgets.WidgetStateProperty.fromMap}
698+ /// This constructor's [resolve] method finds the first [MapEntry] whose
699+ /// key is satisfied by the set of states, and returns its associated value.
700+ /// {@endtemplate}
701+ ///
702+ /// Returns `null` if no keys match, or if [T] is non-nullable,
703+ /// the method throws an [ArgumentError] .
704+ ///
705+ /// {@macro flutter.widgets.WidgetState.any}
706+ ///
707+ /// {@macro flutter.widgets.WidgetStateMap}
708+ const factory WidgetStateProperty .fromMap (WidgetStateMap <T > map) = _WidgetStateMapper <T >;
528709
529710 /// Resolves the value for the given set of states if `value` is a
530711 /// [WidgetStateProperty] , otherwise returns the value itself.
@@ -567,6 +748,13 @@ abstract class WidgetStateProperty<T> {
567748 }
568749 return _LerpProperties <T >(a, b, t, lerpFunction);
569750 }
751+
752+ /// Returns a value of type `T` that depends on [states] .
753+ ///
754+ /// Widgets like [TextButton] and [ElevatedButton] apply this method to their
755+ /// current [WidgetState] s to compute colors and other visual parameters
756+ /// at build time.
757+ T resolve (Set <WidgetState > states);
570758}
571759
572760class _LerpProperties <T > implements WidgetStateProperty <T ?> {
@@ -594,6 +782,96 @@ class _WidgetStatePropertyWith<T> implements WidgetStateProperty<T> {
594782 T resolve (Set <WidgetState > states) => _resolve (states);
595783}
596784
785+ /// A [Map] used to resolve to a single value of type `T` based on
786+ /// the current set of Widget states.
787+ ///
788+ /// {@template flutter.widgets.WidgetStateMap}
789+ /// Example:
790+ ///
791+ /// ```dart
792+ /// // This WidgetStateMap<Color?> resolves to null if no keys match.
793+ /// WidgetStateProperty<Color?>.fromMap(<WidgetStatesConstraint, Color?>{
794+ /// WidgetState.error: Colors.red,
795+ /// WidgetState.hovered & WidgetState.focused: Colors.blueAccent,
796+ /// WidgetState.focused: Colors.blue,
797+ /// ~WidgetState.disabled: Colors.black,
798+ /// });
799+ ///
800+ /// // The same can be accomplished with a WidgetPropertyResolver,
801+ /// // but it's more verbose:
802+ /// WidgetStateProperty.resolveWith<Color?>((Set<WidgetState> states) {
803+ /// if (states.contains(WidgetState.error)) {
804+ /// return Colors.red;
805+ /// } else if (states.contains(WidgetState.hovered) && states.contains(WidgetState.focused)) {
806+ /// return Colors.blueAccent;
807+ /// } else if (states.contains(WidgetState.focused)) {
808+ /// return Colors.blue;
809+ /// } else if (!states.contains(WidgetState.disabled)) {
810+ /// return Colors.black;
811+ /// }
812+ /// return null;
813+ /// });
814+ /// ```
815+ ///
816+ /// A widget state combination can be stored in a variable,
817+ /// and [WidgetState.any] can be used for non-nullable types to ensure
818+ /// that there's a match:
819+ ///
820+ /// ```dart
821+ /// final WidgetStatesConstraint selectedError = WidgetState.selected & WidgetState.error;
822+ ///
823+ /// final WidgetStateProperty<Color> color = WidgetStateProperty<Color>.fromMap(
824+ /// <WidgetStatesConstraint, Color>{
825+ /// selectedError & WidgetState.hovered: Colors.redAccent,
826+ /// selectedError: Colors.red,
827+ /// WidgetState.any: Colors.black,
828+ /// },
829+ /// );
830+ ///
831+ /// // The (more verbose) WidgetPropertyResolver implementation:
832+ /// final WidgetStateProperty<Color> colorResolveWith = WidgetStateProperty.resolveWith<Color>(
833+ /// (Set<WidgetState> states) {
834+ /// if (states.containsAll(<WidgetState>{WidgetState.selected, WidgetState.error})) {
835+ /// if (states.contains(WidgetState.hovered)) {
836+ /// return Colors.redAccent;
837+ /// }
838+ /// return Colors.red;
839+ /// }
840+ /// return Colors.black;
841+ /// },
842+ /// );
843+ /// ```
844+ /// {@endtemplate}
845+ typedef WidgetStateMap <T > = Map <WidgetStatesConstraint , T >;
846+
847+ // A private class, used to create the [WidgetStateProperty.fromMap] constructor.
848+ class _WidgetStateMapper <T > implements WidgetStateProperty <T > {
849+ const _WidgetStateMapper (this .map);
850+
851+ final WidgetStateMap <T > map;
852+
853+ @override
854+ T resolve (Set <WidgetState > states) {
855+ for (final MapEntry <WidgetStatesConstraint , T > entry in map.entries) {
856+ if (entry.key.isSatisfiedBy (states)) {
857+ return entry.value;
858+ }
859+ }
860+
861+ try {
862+ return null as T ;
863+ } on TypeError {
864+ throw ArgumentError (
865+ 'The current set of material states is $states .\n '
866+ 'None of the provided map keys matched this set, '
867+ 'and the type "$T " is non-nullable.\n '
868+ 'Consider using "WidgetStateProperty<$T ?>.fromMap()", '
869+ 'or adding the "WidgetState.any" key to this map.' ,
870+ );
871+ }
872+ }
873+ }
874+
597875/// Convenience class for creating a [WidgetStateProperty] that
598876/// resolves to the given value for all states.
599877///
0 commit comments