Unverified Commit c6f2cea6 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Reland: Added ButtonStyle.foregroundBuilder and ButtonStyle.backgroundBuilder (#142762)

Reland https://github.com/flutter/flutter/pull/141818 with a fix for a special case: If only `background` is specified for `TextButton.styleFrom` or `OutlinedButton.styleFrom` it applies the button's disabled state, i.e. as if the same value had been specified for disabledBackgroundColor.

The change relative to #141818 is the indicated line below:
```dart
final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
  (null, null) => null,
  (_, null) => MaterialStatePropertyAll<Color?>(backgroundColor), // ADDED THIS LINE
  (_, _) => _TextButtonDefaultColor(backgroundColor, disabledBackgroundColor),
};
  ```

This backwards incompatibility cropped up in an internal test, see internal Google issue b/323399158.
parent c13ebf1e
...@@ -374,7 +374,6 @@ final Set<String> _knownMissingTests = <String>{ ...@@ -374,7 +374,6 @@ final Set<String> _knownMissingTests = <String>{
'examples/api/test/material/checkbox/checkbox.1_test.dart', 'examples/api/test/material/checkbox/checkbox.1_test.dart',
'examples/api/test/material/checkbox/checkbox.0_test.dart', 'examples/api/test/material/checkbox/checkbox.0_test.dart',
'examples/api/test/material/navigation_rail/navigation_rail.extended_animation.0_test.dart', 'examples/api/test/material/navigation_rail/navigation_rail.extended_animation.0_test.dart',
'examples/api/test/material/text_button/text_button.0_test.dart',
'examples/api/test/rendering/growth_direction/growth_direction.0_test.dart', 'examples/api/test/rendering/growth_direction/growth_direction.0_test.dart',
'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart', 'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart',
'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart', 'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart',
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/text_button/text_button.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
// The app being tested loads images via HTTP which the test
// framework defeats by default.
setUpAll(() {
HttpOverrides.global = null;
});
testWidgets('TextButtonExample smoke test', (WidgetTester tester) async {
await tester.pumpWidget(const example.TextButtonExampleApp());
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'Enabled'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'Disabled'));
await tester.pumpAndSettle();
// TextButton.icon buttons are _TextButtonWithIcons rather than TextButtons.
// For the purposes of this test, just tapping in the right place is OK.
await tester.tap(find.text('TextButton.icon #1'));
await tester.pumpAndSettle();
await tester.tap(find.text('TextButton.icon #2'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'TextButton #3'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'TextButton #4'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'TextButton #5'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'TextButton #6'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'TextButton #7'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'TextButton #8'));
await tester.pumpAndSettle();
await tester.tap(find.byType(TextButton).last); // Smiley image button
await tester.pumpAndSettle();
await tester.tap(find.byType(Switch).at(0)); // Dark Mode Switch
await tester.pumpAndSettle();
await tester.tap(find.byType(Switch).at(1)); // RTL Text Switch
await tester.pumpAndSettle();
});
}
...@@ -16,6 +16,12 @@ import 'theme_data.dart'; ...@@ -16,6 +16,12 @@ import 'theme_data.dart';
// late BuildContext context; // late BuildContext context;
// typedef MyAppHome = Placeholder; // typedef MyAppHome = Placeholder;
/// The type for [ButtonStyle.backgroundBuilder] and [ButtonStyle.foregroundBuilder].
///
/// The [states] parameter is the button's current pressed/hovered/etc state. The [child] is
/// typically a descendant of the returned widget.
typedef ButtonLayerBuilder = Widget Function(BuildContext context, Set<MaterialState> states, Widget? child);
/// The visual properties that most buttons have in common. /// The visual properties that most buttons have in common.
/// ///
/// Buttons and their themes have a ButtonStyle property which defines the visual /// Buttons and their themes have a ButtonStyle property which defines the visual
...@@ -162,6 +168,8 @@ class ButtonStyle with Diagnosticable { ...@@ -162,6 +168,8 @@ class ButtonStyle with Diagnosticable {
this.enableFeedback, this.enableFeedback,
this.alignment, this.alignment,
this.splashFactory, this.splashFactory,
this.backgroundBuilder,
this.foregroundBuilder,
}); });
/// The style for a button's [Text] widget descendants. /// The style for a button's [Text] widget descendants.
...@@ -315,6 +323,42 @@ class ButtonStyle with Diagnosticable { ...@@ -315,6 +323,42 @@ class ButtonStyle with Diagnosticable {
/// ``` /// ```
final InteractiveInkFeatureFactory? splashFactory; final InteractiveInkFeatureFactory? splashFactory;
/// Creates a widget that becomes the child of the button's [Material]
/// and whose child is the rest of the button, including the button's
/// `child` parameter.
///
/// The widget created by [backgroundBuilder] is constrained to be
/// the same size as the overall button and will appear behind the
/// button's child. The widget created by [foregroundBuilder] is
/// constrained to be the same size as the button's child, i.e. it's
/// inset by [ButtonStyle.padding] and aligned by the button's
/// [ButtonStyle.alignment].
///
/// By default the returned widget is clipped to the Material's [ButtonStyle.shape].
///
/// See also:
///
/// * [foregroundBuilder], to create a widget that's as big as the button's
/// child and is layered behind the child.
/// * [ButtonStyleButton.clipBehavior], for more information about
/// configuring clipping.
final ButtonLayerBuilder? backgroundBuilder;
/// Creates a Widget that contains the button's child parameter which is used
/// instead of the button's child.
///
/// The returned widget is clipped by the button's
/// [ButtonStyle.shape], inset by the button's [ButtonStyle.padding]
/// and aligned by the button's [ButtonStyle.alignment].
///
/// See also:
///
/// * [backgroundBuilder], to create a widget that's as big as the button and
/// is layered behind the button's child.
/// * [ButtonStyleButton.clipBehavior], for more information about
/// configuring clipping.
final ButtonLayerBuilder? foregroundBuilder;
/// Returns a copy of this ButtonStyle with the given fields replaced with /// Returns a copy of this ButtonStyle with the given fields replaced with
/// the new values. /// the new values.
ButtonStyle copyWith({ ButtonStyle copyWith({
...@@ -340,6 +384,8 @@ class ButtonStyle with Diagnosticable { ...@@ -340,6 +384,8 @@ class ButtonStyle with Diagnosticable {
bool? enableFeedback, bool? enableFeedback,
AlignmentGeometry? alignment, AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory, InteractiveInkFeatureFactory? splashFactory,
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) { }) {
return ButtonStyle( return ButtonStyle(
textStyle: textStyle ?? this.textStyle, textStyle: textStyle ?? this.textStyle,
...@@ -364,6 +410,8 @@ class ButtonStyle with Diagnosticable { ...@@ -364,6 +410,8 @@ class ButtonStyle with Diagnosticable {
enableFeedback: enableFeedback ?? this.enableFeedback, enableFeedback: enableFeedback ?? this.enableFeedback,
alignment: alignment ?? this.alignment, alignment: alignment ?? this.alignment,
splashFactory: splashFactory ?? this.splashFactory, splashFactory: splashFactory ?? this.splashFactory,
backgroundBuilder: backgroundBuilder ?? this.backgroundBuilder,
foregroundBuilder: foregroundBuilder ?? this.foregroundBuilder,
); );
} }
...@@ -399,6 +447,8 @@ class ButtonStyle with Diagnosticable { ...@@ -399,6 +447,8 @@ class ButtonStyle with Diagnosticable {
enableFeedback: enableFeedback ?? style.enableFeedback, enableFeedback: enableFeedback ?? style.enableFeedback,
alignment: alignment ?? style.alignment, alignment: alignment ?? style.alignment,
splashFactory: splashFactory ?? style.splashFactory, splashFactory: splashFactory ?? style.splashFactory,
backgroundBuilder: backgroundBuilder ?? style.backgroundBuilder,
foregroundBuilder: foregroundBuilder ?? style.foregroundBuilder,
); );
} }
...@@ -427,6 +477,8 @@ class ButtonStyle with Diagnosticable { ...@@ -427,6 +477,8 @@ class ButtonStyle with Diagnosticable {
enableFeedback, enableFeedback,
alignment, alignment,
splashFactory, splashFactory,
backgroundBuilder,
foregroundBuilder,
]; ];
return Object.hashAll(values); return Object.hashAll(values);
} }
...@@ -461,7 +513,9 @@ class ButtonStyle with Diagnosticable { ...@@ -461,7 +513,9 @@ class ButtonStyle with Diagnosticable {
&& other.animationDuration == animationDuration && other.animationDuration == animationDuration
&& other.enableFeedback == enableFeedback && other.enableFeedback == enableFeedback
&& other.alignment == alignment && other.alignment == alignment
&& other.splashFactory == splashFactory; && other.splashFactory == splashFactory
&& other.backgroundBuilder == backgroundBuilder
&& other.foregroundBuilder == foregroundBuilder;
} }
@override @override
...@@ -488,6 +542,8 @@ class ButtonStyle with Diagnosticable { ...@@ -488,6 +542,8 @@ class ButtonStyle with Diagnosticable {
properties.add(DiagnosticsProperty<Duration>('animationDuration', animationDuration, defaultValue: null)); properties.add(DiagnosticsProperty<Duration>('animationDuration', animationDuration, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null)); properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null));
properties.add(DiagnosticsProperty<ButtonLayerBuilder>('backgroundBuilder', backgroundBuilder, defaultValue: null));
properties.add(DiagnosticsProperty<ButtonLayerBuilder>('foregroundBuilder', foregroundBuilder, defaultValue: null));
} }
/// Linearly interpolate between two [ButtonStyle]s. /// Linearly interpolate between two [ButtonStyle]s.
...@@ -518,6 +574,8 @@ class ButtonStyle with Diagnosticable { ...@@ -518,6 +574,8 @@ class ButtonStyle with Diagnosticable {
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback, enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
alignment: AlignmentGeometry.lerp(a?.alignment, b?.alignment, t), alignment: AlignmentGeometry.lerp(a?.alignment, b?.alignment, t),
splashFactory: t < 0.5 ? a?.splashFactory : b?.splashFactory, splashFactory: t < 0.5 ? a?.splashFactory : b?.splashFactory,
backgroundBuilder: t < 0.5 ? a?.backgroundBuilder : b?.backgroundBuilder,
foregroundBuilder: t < 0.5 ? a?.foregroundBuilder : b?.foregroundBuilder,
); );
} }
......
...@@ -89,8 +89,10 @@ abstract class ButtonStyleButton extends StatefulWidget { ...@@ -89,8 +89,10 @@ abstract class ButtonStyleButton extends StatefulWidget {
/// {@macro flutter.material.Material.clipBehavior} /// {@macro flutter.material.Material.clipBehavior}
/// ///
/// Defaults to [Clip.none]. /// Defaults to [Clip.none] unless [ButtonStyle.backgroundBuilder] or
final Clip clipBehavior; /// [ButtonStyle.foregroundBuilder] is specified. In those
/// cases the default is [Clip.antiAlias].
final Clip? clipBehavior;
/// {@macro flutter.widgets.Focus.focusNode} /// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode; final FocusNode? focusNode;
...@@ -318,6 +320,11 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat ...@@ -318,6 +320,11 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
final AlignmentGeometry? resolvedAlignment = effectiveValue((ButtonStyle? style) => style?.alignment); final AlignmentGeometry? resolvedAlignment = effectiveValue((ButtonStyle? style) => style?.alignment);
final Offset densityAdjustment = resolvedVisualDensity!.baseSizeAdjustment; final Offset densityAdjustment = resolvedVisualDensity!.baseSizeAdjustment;
final InteractiveInkFeatureFactory? resolvedSplashFactory = effectiveValue((ButtonStyle? style) => style?.splashFactory); final InteractiveInkFeatureFactory? resolvedSplashFactory = effectiveValue((ButtonStyle? style) => style?.splashFactory);
final ButtonLayerBuilder? resolvedBackgroundBuilder = effectiveValue((ButtonStyle? style) => style?.backgroundBuilder);
final ButtonLayerBuilder? resolvedForegroundBuilder = effectiveValue((ButtonStyle? style) => style?.foregroundBuilder);
final Clip effectiveClipBehavior = widget.clipBehavior
?? ((resolvedBackgroundBuilder ?? resolvedForegroundBuilder) != null ? Clip.antiAlias : Clip.none);
BoxConstraints effectiveConstraints = resolvedVisualDensity.effectiveConstraints( BoxConstraints effectiveConstraints = resolvedVisualDensity.effectiveConstraints(
BoxConstraints( BoxConstraints(
...@@ -384,6 +391,21 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat ...@@ -384,6 +391,21 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
elevation = resolvedElevation; elevation = resolvedElevation;
backgroundColor = resolvedBackgroundColor; backgroundColor = resolvedBackgroundColor;
Widget effectiveChild = Padding(
padding: padding,
child: Align(
alignment: resolvedAlignment!,
widthFactor: 1.0,
heightFactor: 1.0,
child: resolvedForegroundBuilder != null
? resolvedForegroundBuilder(context, statesController.value, widget.child)
: widget.child,
),
);
if (resolvedBackgroundBuilder != null) {
effectiveChild = resolvedBackgroundBuilder(context, statesController.value, effectiveChild);
}
final Widget result = ConstrainedBox( final Widget result = ConstrainedBox(
constraints: effectiveConstraints, constraints: effectiveConstraints,
child: Material( child: Material(
...@@ -395,7 +417,7 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat ...@@ -395,7 +417,7 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
surfaceTintColor: resolvedSurfaceTintColor, surfaceTintColor: resolvedSurfaceTintColor,
type: resolvedBackgroundColor == null ? MaterialType.transparency : MaterialType.button, type: resolvedBackgroundColor == null ? MaterialType.transparency : MaterialType.button,
animationDuration: resolvedAnimationDuration, animationDuration: resolvedAnimationDuration,
clipBehavior: widget.clipBehavior, clipBehavior: effectiveClipBehavior,
child: InkWell( child: InkWell(
onTap: widget.onPressed, onTap: widget.onPressed,
onLongPress: widget.onLongPress, onLongPress: widget.onLongPress,
...@@ -413,15 +435,7 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat ...@@ -413,15 +435,7 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
statesController: statesController, statesController: statesController,
child: IconTheme.merge( child: IconTheme.merge(
data: IconThemeData(color: resolvedIconColor ?? resolvedForegroundColor, size: resolvedIconSize), data: IconThemeData(color: resolvedIconColor ?? resolvedForegroundColor, size: resolvedIconSize),
child: Padding( child: effectiveChild,
padding: padding,
child: Align(
alignment: resolvedAlignment!,
widthFactor: 1.0,
heightFactor: 1.0,
child: widget.child,
),
),
), ),
), ),
), ),
......
...@@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart'; ...@@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart';
import 'button_style.dart'; import 'button_style.dart';
import 'button_style_button.dart'; import 'button_style_button.dart';
import 'color_scheme.dart'; import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart'; import 'constants.dart';
import 'elevated_button_theme.dart'; import 'elevated_button_theme.dart';
import 'ink_ripple.dart'; import 'ink_ripple.dart';
...@@ -70,7 +71,7 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -70,7 +71,7 @@ class ElevatedButton extends ButtonStyleButton {
super.style, super.style,
super.focusNode, super.focusNode,
super.autofocus = false, super.autofocus = false,
super.clipBehavior = Clip.none, super.clipBehavior,
super.statesController, super.statesController,
required super.child, required super.child,
}); });
...@@ -132,19 +133,26 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -132,19 +133,26 @@ class ElevatedButton extends ButtonStyleButton {
/// ///
/// The [foregroundColor] and [disabledForegroundColor] colors are used /// The [foregroundColor] and [disabledForegroundColor] colors are used
/// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and /// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and
/// a derived [ButtonStyle.overlayColor]. /// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
///
/// If [overlayColor] is specified and its value is [Colors.transparent]
/// then the pressed/focused/hovered highlights are effectively defeated.
/// Otherwise a [MaterialStateProperty] with the same opacities as the
/// default is created.
/// ///
/// The [backgroundColor] and [disabledBackgroundColor] colors are /// The [backgroundColor] and [disabledBackgroundColor] colors are
/// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor]. /// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor].
/// ///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle.mouseCursor] and
/// [iconColor], [disabledIconColor] are used to construct
/// [ButtonStyle.iconColor].
///
/// The button's elevations are defined relative to the [elevation] /// The button's elevations are defined relative to the [elevation]
/// parameter. The disabled elevation is the same as the parameter /// parameter. The disabled elevation is the same as the parameter
/// value, [elevation] + 2 is used when the button is hovered /// value, [elevation] + 2 is used when the button is hovered
/// or focused, and elevation + 6 is used when the button is pressed. /// or focused, and elevation + 6 is used when the button is pressed.
/// ///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle].mouseCursor.
///
/// All of the other parameters are either used directly or used to /// All of the other parameters are either used directly or used to
/// create a [MaterialStateProperty] with a single value for all /// create a [MaterialStateProperty] with a single value for all
/// states. /// states.
...@@ -186,6 +194,9 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -186,6 +194,9 @@ class ElevatedButton extends ButtonStyleButton {
Color? disabledBackgroundColor, Color? disabledBackgroundColor,
Color? shadowColor, Color? shadowColor,
Color? surfaceTintColor, Color? surfaceTintColor,
Color? iconColor,
Color? disabledIconColor,
Color? overlayColor,
double? elevation, double? elevation,
TextStyle? textStyle, TextStyle? textStyle,
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
...@@ -202,32 +213,40 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -202,32 +213,40 @@ class ElevatedButton extends ButtonStyleButton {
bool? enableFeedback, bool? enableFeedback,
AlignmentGeometry? alignment, AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory, InteractiveInkFeatureFactory? splashFactory,
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) { }) {
final Color? background = backgroundColor; final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
final Color? disabledBackground = disabledBackgroundColor; (null, null) => null,
final MaterialStateProperty<Color?>? backgroundColorProp = (background == null && disabledBackground == null) (_, _) => _ElevatedButtonDefaultColor(foregroundColor, disabledForegroundColor),
? null };
: _ElevatedButtonDefaultColor(background, disabledBackground); final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
final Color? foreground = foregroundColor; (null, null) => null,
final Color? disabledForeground = disabledForegroundColor; (_, _) => _ElevatedButtonDefaultColor(backgroundColor, disabledBackgroundColor),
final MaterialStateProperty<Color?>? foregroundColorProp = (foreground == null && disabledForeground == null) };
? null final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
: _ElevatedButtonDefaultColor(foreground, disabledForeground); (null, null) => null,
final MaterialStateProperty<Color?>? overlayColor = (foreground == null) (_, _) => _ElevatedButtonDefaultColor(iconColor, disabledIconColor),
? null };
: _ElevatedButtonDefaultOverlay(foreground); final MaterialStateProperty<Color?>? overlayColorProp = switch ((foregroundColor, overlayColor)) {
final MaterialStateProperty<double>? elevationValue = (elevation == null) (null, null) => null,
? null (_, final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
: _ElevatedButtonDefaultElevation(elevation); (_, _) => _ElevatedButtonDefaultOverlay((overlayColor ?? foregroundColor)!),
};
final MaterialStateProperty<double>? elevationValue = switch (elevation) {
null => null,
_ => _ElevatedButtonDefaultElevation(elevation),
};
final MaterialStateProperty<MouseCursor?> mouseCursor = _ElevatedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor); final MaterialStateProperty<MouseCursor?> mouseCursor = _ElevatedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle( return ButtonStyle(
textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle), textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle),
backgroundColor: backgroundColorProp, backgroundColor: backgroundColorProp,
foregroundColor: foregroundColorProp, foregroundColor: foregroundColorProp,
overlayColor: overlayColor, overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor), shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor), surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp,
elevation: elevationValue, elevation: elevationValue,
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding), padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize), minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
...@@ -242,6 +261,8 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -242,6 +261,8 @@ class ElevatedButton extends ButtonStyleButton {
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
alignment: alignment, alignment: alignment,
splashFactory: splashFactory, splashFactory: splashFactory,
backgroundBuilder: backgroundBuilder,
foregroundBuilder: foregroundBuilder,
); );
} }
...@@ -283,7 +304,7 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -283,7 +304,7 @@ class ElevatedButton extends ButtonStyleButton {
/// * others - Theme.colorScheme.onPrimary /// * others - Theme.colorScheme.onPrimary
/// * `overlayColor` /// * `overlayColor`
/// * hovered - Theme.colorScheme.onPrimary(0.08) /// * hovered - Theme.colorScheme.onPrimary(0.08)
/// * focused or pressed - Theme.colorScheme.onPrimary(0.24) /// * focused or pressed - Theme.colorScheme.onPrimary(0.12)
/// * `shadowColor` - Theme.shadowColor /// * `shadowColor` - Theme.shadowColor
/// * `elevation` /// * `elevation`
/// * disabled - 0 /// * disabled - 0
...@@ -507,13 +528,12 @@ class _ElevatedButtonWithIcon extends ElevatedButton { ...@@ -507,13 +528,12 @@ class _ElevatedButtonWithIcon extends ElevatedButton {
super.style, super.style,
super.focusNode, super.focusNode,
bool? autofocus, bool? autofocus,
Clip? clipBehavior, super.clipBehavior,
super.statesController, super.statesController,
required Widget icon, required Widget icon,
required Widget label, required Widget label,
}) : super( }) : super(
autofocus: autofocus ?? false, autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _ElevatedButtonWithIconChild(icon: icon, label: label, buttonStyle: style), child: _ElevatedButtonWithIconChild(icon: icon, label: label, buttonStyle: style),
); );
......
...@@ -202,19 +202,23 @@ class FilledButton extends ButtonStyleButton { ...@@ -202,19 +202,23 @@ class FilledButton extends ButtonStyleButton {
/// A static convenience method that constructs a filled button /// A static convenience method that constructs a filled button
/// [ButtonStyle] given simple values. /// [ButtonStyle] given simple values.
/// ///
/// The [foregroundColor], and [disabledForegroundColor] colors are used to create a /// The [foregroundColor] and [disabledForegroundColor] colors are used
/// [MaterialStateProperty] [ButtonStyle.foregroundColor] value. The /// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and
/// [backgroundColor] and [disabledBackgroundColor] are used to create a /// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
/// [MaterialStateProperty] [ButtonStyle.backgroundColor] value. ///
/// If [overlayColor] is specified and its value is [Colors.transparent]
/// then the pressed/focused/hovered highlights are effectively defeated.
/// Otherwise a [MaterialStateProperty] with the same opacities as the
/// default is created.
///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle.mouseCursor].
/// ///
/// The button's elevations are defined relative to the [elevation] /// The button's elevations are defined relative to the [elevation]
/// parameter. The disabled elevation is the same as the parameter /// parameter. The disabled elevation is the same as the parameter
/// value, [elevation] + 2 is used when the button is hovered /// value, [elevation] + 2 is used when the button is hovered
/// or focused, and elevation + 6 is used when the button is pressed. /// or focused, and elevation + 6 is used when the button is pressed.
/// ///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle.mouseCursor].
///
/// All of the other parameters are either used directly or used to /// All of the other parameters are either used directly or used to
/// create a [MaterialStateProperty] with a single value for all /// create a [MaterialStateProperty] with a single value for all
/// states. /// states.
...@@ -250,6 +254,9 @@ class FilledButton extends ButtonStyleButton { ...@@ -250,6 +254,9 @@ class FilledButton extends ButtonStyleButton {
Color? disabledBackgroundColor, Color? disabledBackgroundColor,
Color? shadowColor, Color? shadowColor,
Color? surfaceTintColor, Color? surfaceTintColor,
Color? iconColor,
Color? disabledIconColor,
Color? overlayColor,
double? elevation, double? elevation,
TextStyle? textStyle, TextStyle? textStyle,
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
...@@ -266,29 +273,36 @@ class FilledButton extends ButtonStyleButton { ...@@ -266,29 +273,36 @@ class FilledButton extends ButtonStyleButton {
bool? enableFeedback, bool? enableFeedback,
AlignmentGeometry? alignment, AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory, InteractiveInkFeatureFactory? splashFactory,
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) { }) {
final MaterialStateProperty<Color?>? backgroundColorProp = final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
(backgroundColor == null && disabledBackgroundColor == null) (null, null) => null,
? null (_, _) => _FilledButtonDefaultColor(foregroundColor, disabledForegroundColor),
: _FilledButtonDefaultColor(backgroundColor, disabledBackgroundColor); };
final Color? foreground = foregroundColor; final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
final Color? disabledForeground = disabledForegroundColor; (null, null) => null,
final MaterialStateProperty<Color?>? foregroundColorProp = (_, _) => _FilledButtonDefaultColor(backgroundColor, disabledBackgroundColor),
(foreground == null && disabledForeground == null) };
? null final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
: _FilledButtonDefaultColor(foreground, disabledForeground); (null, null) => null,
final MaterialStateProperty<Color?>? overlayColor = (foreground == null) (_, _) => _FilledButtonDefaultColor(iconColor, disabledIconColor),
? null };
: _FilledButtonDefaultOverlay(foreground); 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)!),
};
final MaterialStateProperty<MouseCursor?> mouseCursor = _FilledButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor); final MaterialStateProperty<MouseCursor?> mouseCursor = _FilledButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle( return ButtonStyle(
textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle), textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle),
backgroundColor: backgroundColorProp, backgroundColor: backgroundColorProp,
foregroundColor: foregroundColorProp, foregroundColor: foregroundColorProp,
overlayColor: overlayColor, overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor), shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor), surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp,
elevation: ButtonStyleButton.allOrNull(elevation), elevation: ButtonStyleButton.allOrNull(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding), padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize), minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
...@@ -303,6 +317,8 @@ class FilledButton extends ButtonStyleButton { ...@@ -303,6 +317,8 @@ class FilledButton extends ButtonStyleButton {
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
alignment: alignment, alignment: alignment,
splashFactory: splashFactory, splashFactory: splashFactory,
backgroundBuilder: backgroundBuilder,
foregroundBuilder: foregroundBuilder,
); );
} }
...@@ -515,14 +531,13 @@ class _FilledButtonWithIcon extends FilledButton { ...@@ -515,14 +531,13 @@ class _FilledButtonWithIcon extends FilledButton {
super.style, super.style,
super.focusNode, super.focusNode,
bool? autofocus, bool? autofocus,
Clip? clipBehavior, super.clipBehavior,
super.statesController, super.statesController,
required Widget icon, required Widget icon,
required Widget label, required Widget label,
}) : super( }) : super(
autofocus: autofocus ?? false, autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none, child: _FilledButtonWithIconChild(icon: icon, label: label, buttonStyle: style)
child: _FilledButtonWithIconChild(icon: icon, label: label, buttonStyle: style),
); );
_FilledButtonWithIcon.tonal({ _FilledButtonWithIcon.tonal({
...@@ -534,14 +549,13 @@ class _FilledButtonWithIcon extends FilledButton { ...@@ -534,14 +549,13 @@ class _FilledButtonWithIcon extends FilledButton {
super.style, super.style,
super.focusNode, super.focusNode,
bool? autofocus, bool? autofocus,
Clip? clipBehavior, super.clipBehavior,
super.statesController, super.statesController,
required Widget icon, required Widget icon,
required Widget label, required Widget label,
}) : super.tonal( }) : super.tonal(
autofocus: autofocus ?? false, autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none, child: _FilledButtonWithIconChild(icon: icon, label: label, buttonStyle: style)
child: _FilledButtonWithIconChild(icon: icon, label: label, buttonStyle: style),
); );
@override @override
......
...@@ -168,6 +168,9 @@ abstract class MaterialStateColor extends Color implements MaterialStateProperty ...@@ -168,6 +168,9 @@ abstract class MaterialStateColor extends Color implements MaterialStateProperty
/// specified state. /// specified state.
@override @override
Color resolve(Set<MaterialState> states); Color resolve(Set<MaterialState> states);
/// A constant whose value is [Colors.transparent] for all states.
static const MaterialStateColor transparent = _MaterialStateColorTransparent();
} }
/// A [MaterialStateColor] created from a [MaterialPropertyResolver<Color>] /// A [MaterialStateColor] created from a [MaterialPropertyResolver<Color>]
...@@ -189,6 +192,13 @@ class _MaterialStateColor extends MaterialStateColor { ...@@ -189,6 +192,13 @@ class _MaterialStateColor extends MaterialStateColor {
Color resolve(Set<MaterialState> states) => _resolve(states); Color resolve(Set<MaterialState> states) => _resolve(states);
} }
class _MaterialStateColorTransparent extends MaterialStateColor {
const _MaterialStateColorTransparent() : super(0x00000000);
@override
Color resolve(Set<MaterialState> states) => const Color(0x00000000);
}
/// Defines a [MouseCursor] whose value depends on a set of [MaterialState]s which /// Defines a [MouseCursor] whose value depends on a set of [MaterialState]s which
/// represent the interactive state of a component. /// represent the interactive state of a component.
/// ///
......
...@@ -75,7 +75,7 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -75,7 +75,7 @@ class OutlinedButton extends ButtonStyleButton {
super.style, super.style,
super.focusNode, super.focusNode,
super.autofocus = false, super.autofocus = false,
super.clipBehavior = Clip.none, super.clipBehavior,
super.statesController, super.statesController,
required super.child, required super.child,
}); });
...@@ -129,16 +129,22 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -129,16 +129,22 @@ class OutlinedButton extends ButtonStyleButton {
/// A static convenience method that constructs an outlined button /// A static convenience method that constructs an outlined button
/// [ButtonStyle] given simple values. /// [ButtonStyle] given simple values.
/// ///
///
/// The [foregroundColor] and [disabledForegroundColor] colors are used /// The [foregroundColor] and [disabledForegroundColor] colors are used
/// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and /// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and
/// a derived [ButtonStyle.overlayColor]. /// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
/// ///
/// The [backgroundColor] and [disabledBackgroundColor] colors are /// The [backgroundColor] and [disabledBackgroundColor] colors are
/// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor]. /// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor].
/// ///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor] /// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle.mouseCursor]. /// parameters are used to construct [ButtonStyle.mouseCursor] and
/// [iconColor], [disabledIconColor] are used to construct
/// [ButtonStyle.iconColor].
///
/// If [overlayColor] is specified and its value is [Colors.transparent]
/// then the pressed/focused/hovered highlights are effectively defeated.
/// Otherwise a [MaterialStateProperty] with the same opacities as the
/// default is created.
/// ///
/// All of the other parameters are either used directly or used to /// All of the other parameters are either used directly or used to
/// create a [MaterialStateProperty] with a single value for all /// create a [MaterialStateProperty] with a single value for all
...@@ -169,6 +175,9 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -169,6 +175,9 @@ class OutlinedButton extends ButtonStyleButton {
Color? disabledBackgroundColor, Color? disabledBackgroundColor,
Color? shadowColor, Color? shadowColor,
Color? surfaceTintColor, Color? surfaceTintColor,
Color? iconColor,
Color? disabledIconColor,
Color? overlayColor,
double? elevation, double? elevation,
TextStyle? textStyle, TextStyle? textStyle,
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
...@@ -185,29 +194,37 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -185,29 +194,37 @@ class OutlinedButton extends ButtonStyleButton {
bool? enableFeedback, bool? enableFeedback,
AlignmentGeometry? alignment, AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory, InteractiveInkFeatureFactory? splashFactory,
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) { }) {
final Color? foreground = foregroundColor; final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
final Color? disabledForeground = disabledForegroundColor; (null, null) => null,
final MaterialStateProperty<Color?>? foregroundColorProp = (foreground == null && disabledForeground == null) (_, _) => _OutlinedButtonDefaultColor(foregroundColor, disabledForegroundColor),
? null };
: _OutlinedButtonDefaultColor(foreground, disabledForeground); final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
final MaterialStateProperty<Color?>? backgroundColorProp = (backgroundColor == null && disabledBackgroundColor == null) (null, null) => null,
? null (_, null) => MaterialStatePropertyAll<Color?>(backgroundColor),
: disabledBackgroundColor == null (_, _) => _OutlinedButtonDefaultColor(backgroundColor, disabledBackgroundColor),
? ButtonStyleButton.allOrNull<Color?>(backgroundColor) };
: _OutlinedButtonDefaultColor(backgroundColor, disabledBackgroundColor); final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
final MaterialStateProperty<Color?>? overlayColor = (foreground == null) (null, null) => null,
? null (_, _) => _OutlinedButtonDefaultColor(iconColor, disabledIconColor),
: _OutlinedButtonDefaultOverlay(foreground); };
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)!),
};
final MaterialStateProperty<MouseCursor?> mouseCursor = _OutlinedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor); final MaterialStateProperty<MouseCursor?> mouseCursor = _OutlinedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle( return ButtonStyle(
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle), textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
foregroundColor: foregroundColorProp, foregroundColor: foregroundColorProp,
backgroundColor: backgroundColorProp, backgroundColor: backgroundColorProp,
overlayColor: overlayColor, overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor), shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor), surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp,
elevation: ButtonStyleButton.allOrNull<double>(elevation), elevation: ButtonStyleButton.allOrNull<double>(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding), padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize), minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
...@@ -222,6 +239,8 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -222,6 +239,8 @@ class OutlinedButton extends ButtonStyleButton {
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
alignment: alignment, alignment: alignment,
splashFactory: splashFactory, splashFactory: splashFactory,
backgroundBuilder: backgroundBuilder,
foregroundBuilder: foregroundBuilder,
); );
} }
...@@ -256,7 +275,7 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -256,7 +275,7 @@ class OutlinedButton extends ButtonStyleButton {
/// * disabled - Theme.colorScheme.onSurface(0.38) /// * disabled - Theme.colorScheme.onSurface(0.38)
/// * others - Theme.colorScheme.primary /// * others - Theme.colorScheme.primary
/// * `overlayColor` /// * `overlayColor`
/// * hovered - Theme.colorScheme.primary(0.04) /// * hovered - Theme.colorScheme.primary(0.08)
/// * focused or pressed - Theme.colorScheme.primary(0.12) /// * focused or pressed - Theme.colorScheme.primary(0.12)
/// * `shadowColor` - Theme.shadowColor /// * `shadowColor` - Theme.shadowColor
/// * `elevation` - 0 /// * `elevation` - 0
...@@ -339,9 +358,7 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -339,9 +358,7 @@ class OutlinedButton extends ButtonStyleButton {
padding: _scaledPadding(context), padding: _scaledPadding(context),
minimumSize: const Size(64, 36), minimumSize: const Size(64, 36),
maximumSize: Size.infinite, maximumSize: Size.infinite,
side: BorderSide( side: BorderSide(color: colorScheme.onSurface.withOpacity(0.12)),
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.12),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))),
enabledMouseCursor: SystemMouseCursors.click, enabledMouseCursor: SystemMouseCursors.click,
disabledMouseCursor: SystemMouseCursors.basic, disabledMouseCursor: SystemMouseCursors.basic,
...@@ -434,13 +451,12 @@ class _OutlinedButtonWithIcon extends OutlinedButton { ...@@ -434,13 +451,12 @@ class _OutlinedButtonWithIcon extends OutlinedButton {
super.style, super.style,
super.focusNode, super.focusNode,
bool? autofocus, bool? autofocus,
Clip? clipBehavior, super.clipBehavior,
super.statesController, super.statesController,
required Widget icon, required Widget icon,
required Widget label, required Widget label,
}) : super( }) : super(
autofocus: autofocus ?? false, autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _OutlinedButtonWithIconChild(icon: icon, label: label, buttonStyle: style), child: _OutlinedButtonWithIconChild(icon: icon, label: label, buttonStyle: style),
); );
......
...@@ -50,8 +50,9 @@ import 'theme_data.dart'; ...@@ -50,8 +50,9 @@ import 'theme_data.dart';
/// button will be disabled, it will not react to touch. /// button will be disabled, it will not react to touch.
/// ///
/// {@tool dartpad} /// {@tool dartpad}
/// This sample shows how to render a disabled TextButton, an enabled TextButton /// This sample shows various ways to configure TextButtons, from the
/// and lastly a TextButton with gradient background. /// simplest default appearance to versions that don't resemble
/// Material Design at all.
/// ///
/// ** See code in examples/api/lib/material/text_button/text_button.0.dart ** /// ** See code in examples/api/lib/material/text_button/text_button.0.dart **
/// {@end-tool} /// {@end-tool}
...@@ -82,7 +83,7 @@ class TextButton extends ButtonStyleButton { ...@@ -82,7 +83,7 @@ class TextButton extends ButtonStyleButton {
super.style, super.style,
super.focusNode, super.focusNode,
super.autofocus = false, super.autofocus = false,
super.clipBehavior = Clip.none, super.clipBehavior,
super.statesController, super.statesController,
super.isSemanticButton, super.isSemanticButton,
required Widget super.child, required Widget super.child,
...@@ -142,13 +143,20 @@ class TextButton extends ButtonStyleButton { ...@@ -142,13 +143,20 @@ class TextButton extends ButtonStyleButton {
/// ///
/// The [foregroundColor] and [disabledForegroundColor] colors are used /// The [foregroundColor] and [disabledForegroundColor] colors are used
/// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and /// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and
/// a derived [ButtonStyle.overlayColor]. /// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
/// ///
/// The [backgroundColor] and [disabledBackgroundColor] colors are /// The [backgroundColor] and [disabledBackgroundColor] colors are
/// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor]. /// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor].
/// ///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor] /// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle.mouseCursor]. /// parameters are used to construct [ButtonStyle.mouseCursor] and
/// [iconColor], [disabledIconColor] are used to construct
/// [ButtonStyle.iconColor].
///
/// If [overlayColor] is specified and its value is [Colors.transparent]
/// then the pressed/focused/hovered highlights are effectively defeated.
/// Otherwise a [MaterialStateProperty] with the same opacities as the
/// default is created.
/// ///
/// All of the other parameters are either used directly or used to /// All of the other parameters are either used directly or used to
/// create a [MaterialStateProperty] with a single value for all /// create a [MaterialStateProperty] with a single value for all
...@@ -180,6 +188,7 @@ class TextButton extends ButtonStyleButton { ...@@ -180,6 +188,7 @@ class TextButton extends ButtonStyleButton {
Color? surfaceTintColor, Color? surfaceTintColor,
Color? iconColor, Color? iconColor,
Color? disabledIconColor, Color? disabledIconColor,
Color? overlayColor,
double? elevation, double? elevation,
TextStyle? textStyle, TextStyle? textStyle,
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
...@@ -196,32 +205,35 @@ class TextButton extends ButtonStyleButton { ...@@ -196,32 +205,35 @@ class TextButton extends ButtonStyleButton {
bool? enableFeedback, bool? enableFeedback,
AlignmentGeometry? alignment, AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory, InteractiveInkFeatureFactory? splashFactory,
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) { }) {
final Color? foreground = foregroundColor; final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
final Color? disabledForeground = disabledForegroundColor; (null, null) => null,
final MaterialStateProperty<Color?>? foregroundColorProp = (foreground == null && disabledForeground == null) (_, _) => _TextButtonDefaultColor(foregroundColor, disabledForegroundColor),
? null };
: _TextButtonDefaultColor(foreground, disabledForeground); final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
final MaterialStateProperty<Color?>? backgroundColorProp = (backgroundColor == null && disabledBackgroundColor == null) (null, null) => null,
? null (_, null) => MaterialStatePropertyAll<Color?>(backgroundColor),
: disabledBackgroundColor == null (_, _) => _TextButtonDefaultColor(backgroundColor, disabledBackgroundColor),
? ButtonStyleButton.allOrNull<Color?>(backgroundColor) };
: _TextButtonDefaultColor(backgroundColor, disabledBackgroundColor); final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
final MaterialStateProperty<Color?>? overlayColor = (foreground == null) (null, null) => null,
? null (_, null) => MaterialStatePropertyAll<Color?>(iconColor),
: _TextButtonDefaultOverlay(foreground); (_, _) => _TextButtonDefaultColor(iconColor, disabledIconColor),
final MaterialStateProperty<Color?>? iconColorProp = (iconColor == null && disabledIconColor == null) };
? null final MaterialStateProperty<Color?>? overlayColorProp = switch ((foregroundColor, overlayColor)) {
: disabledIconColor == null (null, null) => null,
? ButtonStyleButton.allOrNull<Color?>(iconColor) (_, final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
: _TextButtonDefaultIconColor(iconColor, disabledIconColor); (_, _) => _TextButtonDefaultOverlay((overlayColor ?? foregroundColor)!),
};
final MaterialStateProperty<MouseCursor?> mouseCursor = _TextButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor); final MaterialStateProperty<MouseCursor?> mouseCursor = _TextButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle( return ButtonStyle(
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle), textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
foregroundColor: foregroundColorProp, foregroundColor: foregroundColorProp,
backgroundColor: backgroundColorProp, backgroundColor: backgroundColorProp,
overlayColor: overlayColor, overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor), shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor), surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp, iconColor: iconColorProp,
...@@ -239,6 +251,8 @@ class TextButton extends ButtonStyleButton { ...@@ -239,6 +251,8 @@ class TextButton extends ButtonStyleButton {
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
alignment: alignment, alignment: alignment,
splashFactory: splashFactory, splashFactory: splashFactory,
backgroundBuilder: backgroundBuilder,
foregroundBuilder: foregroundBuilder,
); );
} }
...@@ -279,7 +293,7 @@ class TextButton extends ButtonStyleButton { ...@@ -279,7 +293,7 @@ class TextButton extends ButtonStyleButton {
/// * disabled - Theme.colorScheme.onSurface(0.38) /// * disabled - Theme.colorScheme.onSurface(0.38)
/// * others - Theme.colorScheme.primary /// * others - Theme.colorScheme.primary
/// * `overlayColor` /// * `overlayColor`
/// * hovered - Theme.colorScheme.primary(0.04) /// * hovered - Theme.colorScheme.primary(0.08)
/// * focused or pressed - Theme.colorScheme.primary(0.12) /// * focused or pressed - Theme.colorScheme.primary(0.12)
/// * `shadowColor` - Theme.shadowColor /// * `shadowColor` - Theme.shadowColor
/// * `elevation` - 0 /// * `elevation` - 0
...@@ -453,27 +467,6 @@ class _TextButtonDefaultOverlay extends MaterialStateProperty<Color?> { ...@@ -453,27 +467,6 @@ class _TextButtonDefaultOverlay extends MaterialStateProperty<Color?> {
} }
} }
@immutable
class _TextButtonDefaultIconColor extends MaterialStateProperty<Color?> {
_TextButtonDefaultIconColor(this.iconColor, this.disabledIconColor);
final Color? iconColor;
final Color? disabledIconColor;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledIconColor;
}
return iconColor;
}
@override
String toString() {
return '{disabled: $disabledIconColor, color: $iconColor}';
}
}
@immutable @immutable
class _TextButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable { class _TextButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable {
_TextButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor); _TextButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
...@@ -500,13 +493,12 @@ class _TextButtonWithIcon extends TextButton { ...@@ -500,13 +493,12 @@ class _TextButtonWithIcon extends TextButton {
super.style, super.style,
super.focusNode, super.focusNode,
bool? autofocus, bool? autofocus,
Clip? clipBehavior, super.clipBehavior,
super.statesController, super.statesController,
required Widget icon, required Widget icon,
required Widget label, required Widget label,
}) : super( }) : super(
autofocus: autofocus ?? false, autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _TextButtonWithIconChild(icon: icon, label: label, buttonStyle: style), child: _TextButtonWithIconChild(icon: icon, label: label, buttonStyle: style),
); );
......
...@@ -1981,6 +1981,203 @@ void main() { ...@@ -1981,6 +1981,203 @@ void main() {
expect(controller.value, <MaterialState>{MaterialState.disabled}); expect(controller.value, <MaterialState>{MaterialState.disabled});
expect(count, 1); expect(count, 1);
}); });
testWidgets('ElevatedButton backgroundBuilder and foregroundBuilder', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: backgroundColor,
),
child: child,
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: foregroundColor,
),
child: child,
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
BoxDecoration boxDecorationOf(Finder finder) {
return tester.widget<DecoratedBox>(finder).decoration as BoxDecoration;
}
final Finder decorations = find.descendant(
of: find.byType(ElevatedButton),
matching: find.byType(DecoratedBox),
);
expect(boxDecorationOf(decorations.at(0)).color, backgroundColor);
expect(boxDecorationOf(decorations.at(1)).color, foregroundColor);
Text textChildOf(Finder finder) {
return tester.widget<Text>(
find.descendant(
of: finder,
matching: find.byType(Text),
),
);
}
expect(textChildOf(decorations.at(0)).data, 'button');
expect(textChildOf(decorations.at(1)).data, 'button');
});
testWidgets('ElevatedButton backgroundBuilder drops button child and foregroundBuilder return value', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
),
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder background = find.descendant(
of: find.byType(ElevatedButton),
matching: find.byType(DecoratedBox),
);
expect(background, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('ElevatedButton foregroundBuilder drops button child', (WidgetTester tester) async {
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder foreground = find.descendant(
of: find.byType(ElevatedButton),
matching: find.byType(DecoratedBox),
);
expect(foreground, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('ElevatedButton foreground and background builders are applied to the correct states', (WidgetTester tester) async {
Set<MaterialState> foregroundStates = <MaterialState>{};
Set<MaterialState> backgroundStates = <MaterialState>{};
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: ElevatedButton(
style: ButtonStyle(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
backgroundStates = states;
return child!;
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
foregroundStates = states;
return child!;
},
),
onPressed: () {},
focusNode: focusNode,
child: const Text('button'),
),
),
),
),
);
// Default.
expect(backgroundStates.isEmpty, isTrue);
expect(foregroundStates.isEmpty, isTrue);
const Set<MaterialState> focusedStates = <MaterialState>{MaterialState.focused};
const Set<MaterialState> focusedHoveredStates = <MaterialState>{MaterialState.focused, MaterialState.hovered};
const Set<MaterialState> focusedHoveredPressedStates = <MaterialState>{MaterialState.focused, MaterialState.hovered, MaterialState.pressed};
bool sameStates(Set<MaterialState> expectedValue, Set<MaterialState> actualValue) {
return expectedValue.difference(actualValue).isEmpty && actualValue.difference(expectedValue).isEmpty;
}
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(sameStates(focusedStates, backgroundStates), isTrue);
expect(sameStates(focusedStates, foregroundStates), isTrue);
// Hovered.
final Offset center = tester.getCenter(find.byType(ElevatedButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(sameStates(focusedHoveredStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredStates, foregroundStates), isTrue);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(sameStates(focusedHoveredPressedStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredPressedStates, foregroundStates), isTrue);
focusNode.dispose();
});
} }
TextStyle _iconStyle(WidgetTester tester, IconData icon) { TextStyle _iconStyle(WidgetTester tester, IconData icon) {
......
...@@ -2092,6 +2092,202 @@ void main() { ...@@ -2092,6 +2092,202 @@ void main() {
expect(count, 1); expect(count, 1);
}); });
testWidgets('FilledButton backgroundBuilder and foregroundBuilder', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
style: FilledButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: backgroundColor,
),
child: child,
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: foregroundColor,
),
child: child,
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
BoxDecoration boxDecorationOf(Finder finder) {
return tester.widget<DecoratedBox>(finder).decoration as BoxDecoration;
}
final Finder decorations = find.descendant(
of: find.byType(FilledButton),
matching: find.byType(DecoratedBox),
);
expect(boxDecorationOf(decorations.at(0)).color, backgroundColor);
expect(boxDecorationOf(decorations.at(1)).color, foregroundColor);
Text textChildOf(Finder finder) {
return tester.widget<Text>(
find.descendant(
of: finder,
matching: find.byType(Text),
),
);
}
expect(textChildOf(decorations.at(0)).data, 'button');
expect(textChildOf(decorations.at(1)).data, 'button');
});
testWidgets('FilledButton backgroundBuilder drops button child and foregroundBuilder return value', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
style: FilledButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
),
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder background = find.descendant(
of: find.byType(FilledButton),
matching: find.byType(DecoratedBox),
);
expect(background, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('FilledButton foregroundBuilder drops button child', (WidgetTester tester) async {
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
style: FilledButton.styleFrom(
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder foreground = find.descendant(
of: find.byType(FilledButton),
matching: find.byType(DecoratedBox),
);
expect(foreground, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('FilledButton foreground and background builders are applied to the correct states', (WidgetTester tester) async {
Set<MaterialState> foregroundStates = <MaterialState>{};
Set<MaterialState> backgroundStates = <MaterialState>{};
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: FilledButton(
style: ButtonStyle(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
backgroundStates = states;
return child!;
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
foregroundStates = states;
return child!;
},
),
onPressed: () {},
focusNode: focusNode,
child: const Text('button'),
),
),
),
),
);
// Default.
expect(backgroundStates.isEmpty, isTrue);
expect(foregroundStates.isEmpty, isTrue);
const Set<MaterialState> focusedStates = <MaterialState>{MaterialState.focused};
const Set<MaterialState> focusedHoveredStates = <MaterialState>{MaterialState.focused, MaterialState.hovered};
const Set<MaterialState> focusedHoveredPressedStates = <MaterialState>{MaterialState.focused, MaterialState.hovered, MaterialState.pressed};
bool sameStates(Set<MaterialState> expectedValue, Set<MaterialState> actualValue) {
return expectedValue.difference(actualValue).isEmpty && actualValue.difference(expectedValue).isEmpty;
}
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(sameStates(focusedStates, backgroundStates), isTrue);
expect(sameStates(focusedStates, foregroundStates), isTrue);
// Hovered.
final Offset center = tester.getCenter(find.byType(FilledButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(sameStates(focusedHoveredStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredStates, foregroundStates), isTrue);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(sameStates(focusedHoveredPressedStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredPressedStates, foregroundStates), isTrue);
focusNode.dispose();
});
} }
TextStyle _iconStyle(WidgetTester tester, IconData icon) { TextStyle _iconStyle(WidgetTester tester, IconData icon) {
......
...@@ -2131,6 +2131,232 @@ void main() { ...@@ -2131,6 +2131,232 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
testWidgets('OutlinedButton backgroundBuilder and foregroundBuilder', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: backgroundColor,
),
child: child,
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: foregroundColor,
),
child: child,
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
BoxDecoration boxDecorationOf(Finder finder) {
return tester.widget<DecoratedBox>(finder).decoration as BoxDecoration;
}
final Finder decorations = find.descendant(
of: find.byType(OutlinedButton),
matching: find.byType(DecoratedBox),
);
expect(boxDecorationOf(decorations.at(0)).color, backgroundColor);
expect(boxDecorationOf(decorations.at(1)).color, foregroundColor);
Text textChildOf(Finder finder) {
return tester.widget<Text>(
find.descendant(
of: finder,
matching: find.byType(Text),
),
);
}
expect(textChildOf(decorations.at(0)).data, 'button');
expect(textChildOf(decorations.at(1)).data, 'button');
});
testWidgets('OutlinedButton backgroundBuilder drops button child and foregroundBuilder return value', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
),
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder background = find.descendant(
of: find.byType(OutlinedButton),
matching: find.byType(DecoratedBox),
);
expect(background, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('OutlinedButton foregroundBuilder drops button child', (WidgetTester tester) async {
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder foreground = find.descendant(
of: find.byType(OutlinedButton),
matching: find.byType(DecoratedBox),
);
expect(foreground, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('OutlinedButton foreground and background builders are applied to the correct states', (WidgetTester tester) async {
Set<MaterialState> foregroundStates = <MaterialState>{};
Set<MaterialState> backgroundStates = <MaterialState>{};
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: OutlinedButton(
style: ButtonStyle(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
backgroundStates = states;
return child!;
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
foregroundStates = states;
return child!;
},
),
onPressed: () {},
focusNode: focusNode,
child: const Text('button'),
),
),
),
),
);
// Default.
expect(backgroundStates.isEmpty, isTrue);
expect(foregroundStates.isEmpty, isTrue);
const Set<MaterialState> focusedStates = <MaterialState>{MaterialState.focused};
const Set<MaterialState> focusedHoveredStates = <MaterialState>{MaterialState.focused, MaterialState.hovered};
const Set<MaterialState> focusedHoveredPressedStates = <MaterialState>{MaterialState.focused, MaterialState.hovered, MaterialState.pressed};
bool sameStates(Set<MaterialState> expectedValue, Set<MaterialState> actualValue) {
return expectedValue.difference(actualValue).isEmpty && actualValue.difference(expectedValue).isEmpty;
}
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(sameStates(focusedStates, backgroundStates), isTrue);
expect(sameStates(focusedStates, foregroundStates), isTrue);
// Hovered.
final Offset center = tester.getCenter(find.byType(OutlinedButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(sameStates(focusedHoveredStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredStates, foregroundStates), isTrue);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(sameStates(focusedHoveredPressedStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredPressedStates, foregroundStates), isTrue);
focusNode.dispose();
});
testWidgets('OutlinedButton styleFrom backgroundColor special case', (WidgetTester tester) async {
// Regression test for an internal Google issue: b/323399158
const Color backgroundColor = Color(0xFF000022);
Widget buildFrame({ VoidCallback? onPressed }) {
return Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: backgroundColor,
),
onPressed: () { },
child: const Text('button'),
),
);
}
await tester.pumpWidget(buildFrame(onPressed: () { })); // enabled
final Material material = tester.widget<Material>(find.descendant(
of: find.byType(OutlinedButton),
matching: find.byType(Material),
));
expect(material.color, backgroundColor);
await tester.pumpWidget(buildFrame()); // onPressed: null - disabled
expect(material.color, backgroundColor);
});
} }
TextStyle _iconStyle(WidgetTester tester, IconData icon) { TextStyle _iconStyle(WidgetTester tester, IconData icon) {
......
...@@ -1964,6 +1964,232 @@ void main() { ...@@ -1964,6 +1964,232 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
testWidgets('TextButton backgroundBuilder and foregroundBuilder', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: TextButton(
style: TextButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: backgroundColor,
),
child: child,
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: foregroundColor,
),
child: child,
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
BoxDecoration boxDecorationOf(Finder finder) {
return tester.widget<DecoratedBox>(finder).decoration as BoxDecoration;
}
final Finder decorations = find.descendant(
of: find.byType(TextButton),
matching: find.byType(DecoratedBox),
);
expect(boxDecorationOf(decorations.at(0)).color, backgroundColor);
expect(boxDecorationOf(decorations.at(1)).color, foregroundColor);
Text textChildOf(Finder finder) {
return tester.widget<Text>(
find.descendant(
of: finder,
matching: find.byType(Text),
),
);
}
expect(textChildOf(decorations.at(0)).data, 'button');
expect(textChildOf(decorations.at(1)).data, 'button');
});
testWidgets('TextButton backgroundBuilder drops button child and foregroundBuilder return value', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: TextButton(
style: TextButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
),
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder background = find.descendant(
of: find.byType(TextButton),
matching: find.byType(DecoratedBox),
);
expect(background, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('TextButton foregroundBuilder drops button child', (WidgetTester tester) async {
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: TextButton(
style: TextButton.styleFrom(
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder foreground = find.descendant(
of: find.byType(TextButton),
matching: find.byType(DecoratedBox),
);
expect(foreground, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('TextButton foreground and background builders are applied to the correct states', (WidgetTester tester) async {
Set<MaterialState> foregroundStates = <MaterialState>{};
Set<MaterialState> backgroundStates = <MaterialState>{};
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: TextButton(
style: ButtonStyle(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
backgroundStates = states;
return child!;
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
foregroundStates = states;
return child!;
},
),
onPressed: () {},
focusNode: focusNode,
child: const Text('button'),
),
),
),
),
);
// Default.
expect(backgroundStates.isEmpty, isTrue);
expect(foregroundStates.isEmpty, isTrue);
const Set<MaterialState> focusedStates = <MaterialState>{MaterialState.focused};
const Set<MaterialState> focusedHoveredStates = <MaterialState>{MaterialState.focused, MaterialState.hovered};
const Set<MaterialState> focusedHoveredPressedStates = <MaterialState>{MaterialState.focused, MaterialState.hovered, MaterialState.pressed};
bool sameStates(Set<MaterialState> expectedValue, Set<MaterialState> actualValue) {
return expectedValue.difference(actualValue).isEmpty && actualValue.difference(expectedValue).isEmpty;
}
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(sameStates(focusedStates, backgroundStates), isTrue);
expect(sameStates(focusedStates, foregroundStates), isTrue);
// Hovered.
final Offset center = tester.getCenter(find.byType(TextButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(sameStates(focusedHoveredStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredStates, foregroundStates), isTrue);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(sameStates(focusedHoveredPressedStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredPressedStates, foregroundStates), isTrue);
focusNode.dispose();
});
testWidgets('TextButton styleFrom backgroundColor special case', (WidgetTester tester) async {
// Regression test for an internal Google issue: b/323399158
const Color backgroundColor = Color(0xFF000022);
Widget buildFrame({ VoidCallback? onPressed }) {
return Directionality(
textDirection: TextDirection.ltr,
child: TextButton(
style: TextButton.styleFrom(
backgroundColor: backgroundColor,
),
onPressed: () { },
child: const Text('button'),
),
);
}
await tester.pumpWidget(buildFrame(onPressed: () { })); // enabled
final Material material = tester.widget<Material>(find.descendant(
of: find.byType(TextButton),
matching: find.byType(Material),
));
expect(material.color, backgroundColor);
await tester.pumpWidget(buildFrame()); // onPressed: null - disabled
expect(material.color, backgroundColor);
});
} }
TextStyle? _iconStyle(WidgetTester tester, IconData icon) { TextStyle? _iconStyle(WidgetTester tester, IconData icon) {
......
...@@ -104,6 +104,15 @@ void main() { ...@@ -104,6 +104,15 @@ void main() {
const bool enableFeedback = false; const bool enableFeedback = false;
const AlignmentGeometry alignment = Alignment.centerLeft; const AlignmentGeometry alignment = Alignment.centerLeft;
final Key backgroundKey = UniqueKey();
final Key foregroundKey = UniqueKey();
Widget backgroundBuilder(BuildContext context, Set<MaterialState> states, Widget? child) {
return KeyedSubtree(key: backgroundKey, child: child!);
}
Widget foregroundBuilder(BuildContext context, Set<MaterialState> states, Widget? child) {
return KeyedSubtree(key: foregroundKey, child: child!);
}
final ButtonStyle style = TextButton.styleFrom( final ButtonStyle style = TextButton.styleFrom(
foregroundColor: foregroundColor, foregroundColor: foregroundColor,
disabledForegroundColor: disabledColor, disabledForegroundColor: disabledColor,
...@@ -122,6 +131,8 @@ void main() { ...@@ -122,6 +131,8 @@ void main() {
animationDuration: animationDuration, animationDuration: animationDuration,
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
alignment: alignment, alignment: alignment,
backgroundBuilder: backgroundBuilder,
foregroundBuilder: foregroundBuilder,
); );
Widget buildFrame({ ButtonStyle? buttonStyle, ButtonStyle? themeStyle, ButtonStyle? overallStyle }) { Widget buildFrame({ ButtonStyle? buttonStyle, ButtonStyle? themeStyle, ButtonStyle? overallStyle }) {
...@@ -185,6 +196,8 @@ void main() { ...@@ -185,6 +196,8 @@ void main() {
expect(tester.getSize(find.byType(TextButton)), const Size(200, 200)); expect(tester.getSize(find.byType(TextButton)), const Size(200, 200));
final Align align = tester.firstWidget<Align>(find.ancestor(of: find.text('button'), matching: find.byType(Align))); final Align align = tester.firstWidget<Align>(find.ancestor(of: find.text('button'), matching: find.byType(Align)));
expect(align.alignment, alignment); expect(align.alignment, alignment);
expect(find.descendant(of: findMaterial, matching: find.byKey(backgroundKey)), findsOneWidget);
expect(find.descendant(of: findInkWell, matching: find.byKey(foregroundKey)), findsOneWidget);
} }
testWidgets('Button style overrides defaults', (WidgetTester tester) async { testWidgets('Button style overrides defaults', (WidgetTester tester) async {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment