// 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:math' as math; import 'dart:ui' show lerpDouble; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'button_style.dart'; import 'button_style_button.dart'; import 'color_scheme.dart'; import 'colors.dart'; import 'constants.dart'; import 'filled_button_theme.dart'; import 'ink_well.dart'; import 'material_state.dart'; import 'theme.dart'; import 'theme_data.dart'; enum _FilledButtonVariant { filled, tonal } /// A Material Design filled button. /// /// Filled buttons have the most visual impact after the [FloatingActionButton], /// and should be used for important, final actions that complete a flow, /// like **Save**, **Join now**, or **Confirm**. /// /// A filled button is a label [child] displayed on a [Material] /// widget. The label's [Text] and [Icon] widgets are displayed in /// [style]'s [ButtonStyle.foregroundColor] and the button's filled /// background is the [ButtonStyle.backgroundColor]. /// /// The filled button's default style is defined by /// [defaultStyleOf]. The style of this filled button can be /// overridden with its [style] parameter. The style of all filled /// buttons in a subtree can be overridden with the /// [FilledButtonTheme], and the style of all of the filled /// buttons in an app can be overridden with the [Theme]'s /// [ThemeData.filledButtonTheme] property. /// /// The static [styleFrom] method is a convenient way to create a /// filled button [ButtonStyle] from simple values. /// /// If [onPressed] and [onLongPress] callbacks are null, then the /// button will be disabled. /// /// To create a 'filled tonal' button, use [FilledButton.tonal]. /// /// {@tool dartpad} /// This sample produces enabled and disabled filled and filled tonal /// buttons. /// /// ** See code in examples/api/lib/material/filled_button/filled_button.0.dart ** /// {@end-tool} /// /// See also: /// /// * [ElevatedButton], a filled button whose material elevates when pressed. /// * [OutlinedButton], a button with an outlined border and no fill color. /// * [TextButton], a button with no outline or fill color. /// * <https://material.io/design/components/buttons.html> /// * <https://m3.material.io/components/buttons> class FilledButton extends ButtonStyleButton { /// Create a FilledButton. /// /// The [autofocus] and [clipBehavior] arguments must not be null. const FilledButton({ super.key, required super.onPressed, super.onLongPress, super.onHover, super.onFocusChange, super.style, super.focusNode, super.autofocus = false, super.clipBehavior = Clip.none, super.statesController, required super.child, }) : _variant = _FilledButtonVariant.filled; /// Create a filled button from [icon] and [label]. /// /// The icon and label are arranged in a row with padding at the start and end /// and a gap between them. /// /// The [icon] and [label] arguments must not be null. factory FilledButton.icon({ Key? key, required VoidCallback? onPressed, VoidCallback? onLongPress, ValueChanged<bool>? onHover, ValueChanged<bool>? onFocusChange, ButtonStyle? style, FocusNode? focusNode, bool? autofocus, Clip? clipBehavior, MaterialStatesController? statesController, required Widget icon, required Widget label, }) = _FilledButtonWithIcon; /// Create a tonal variant of FilledButton. /// /// A filled tonal button is an alternative middle ground between /// [FilledButton] and [OutlinedButton]. They’re useful in contexts where /// a lower-priority button requires slightly more emphasis than an /// outline would give, such as "Next" in an onboarding flow. /// /// The [autofocus] and [clipBehavior] arguments must not be null. const FilledButton.tonal({ super.key, required super.onPressed, super.onLongPress, super.onHover, super.onFocusChange, super.style, super.focusNode, super.autofocus = false, super.clipBehavior = Clip.none, super.statesController, required super.child, }) : _variant = _FilledButtonVariant.tonal; /// Create a filled tonal button from [icon] and [label]. /// /// The icon and label are arranged in a row with padding at the start and end /// and a gap between them. /// /// The [icon] and [label] arguments must not be null. factory FilledButton.tonalIcon({ Key? key, required VoidCallback? onPressed, VoidCallback? onLongPress, ValueChanged<bool>? onHover, ValueChanged<bool>? onFocusChange, ButtonStyle? style, FocusNode? focusNode, bool? autofocus, Clip? clipBehavior, MaterialStatesController? statesController, required Widget icon, required Widget label, }) { return _FilledButtonWithIcon.tonal( key: key, onPressed: onPressed, onLongPress: onLongPress, onHover: onHover, onFocusChange: onFocusChange, style: style, focusNode: focusNode, autofocus: autofocus, clipBehavior: clipBehavior, statesController: statesController, icon: icon, label: label, ); } /// A static convenience method that constructs a filled button /// [ButtonStyle] given simple values. /// /// The [foregroundColor], and [disabledForegroundColor] colors are used to create a /// [MaterialStateProperty] [ButtonStyle.foregroundColor] value. The /// [backgroundColor] and [disabledBackgroundColor] are used to create a /// [MaterialStateProperty] [ButtonStyle.backgroundColor] value. /// /// The button's elevations are defined relative to the [elevation] /// parameter. The disabled elevation is the same as the parameter /// value, [elevation] + 2 is used when the button is hovered /// 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 /// create a [MaterialStateProperty] with a single value for all /// states. /// /// All parameters default to null, by default this method returns /// a [ButtonStyle] that doesn't override anything. /// /// For example, to override the default text and icon colors for a /// [FilledButton], as well as its overlay color, with all of the /// standard opacity adjustments for the pressed, focused, and /// hovered states, one could write: /// /// ```dart /// FilledButton( /// style: FilledButton.styleFrom(foregroundColor: Colors.green), /// onPressed: () {}, /// child: const Text('Filled button'), /// ); /// ``` /// /// or for a Filled tonal variant: /// ```dart /// FilledButton.tonal( /// style: FilledButton.styleFrom(foregroundColor: Colors.green), /// onPressed: () {}, /// child: const Text('Filled tonal button'), /// ); /// ``` static ButtonStyle styleFrom({ Color? foregroundColor, Color? backgroundColor, Color? disabledForegroundColor, Color? disabledBackgroundColor, Color? shadowColor, Color? surfaceTintColor, double? elevation, TextStyle? textStyle, EdgeInsetsGeometry? padding, Size? minimumSize, Size? fixedSize, Size? maximumSize, BorderSide? side, OutlinedBorder? shape, MouseCursor? enabledMouseCursor, MouseCursor? disabledMouseCursor, VisualDensity? visualDensity, MaterialTapTargetSize? tapTargetSize, Duration? animationDuration, bool? enableFeedback, AlignmentGeometry? alignment, InteractiveInkFeatureFactory? splashFactory, }) { final MaterialStateProperty<Color?>? backgroundColorProp = (backgroundColor == null && disabledBackgroundColor == null) ? null : _FilledButtonDefaultColor(backgroundColor, disabledBackgroundColor); final Color? foreground = foregroundColor; final Color? disabledForeground = disabledForegroundColor; final MaterialStateProperty<Color?>? foregroundColorProp = (foreground == null && disabledForeground == null) ? null : _FilledButtonDefaultColor(foreground, disabledForeground); final MaterialStateProperty<Color?>? overlayColor = (foreground == null) ? null : _FilledButtonDefaultOverlay(foreground); final MaterialStateProperty<MouseCursor?>? mouseCursor = (enabledMouseCursor == null && disabledMouseCursor == null) ? null : _FilledButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor); return ButtonStyle( textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle), backgroundColor: backgroundColorProp, foregroundColor: foregroundColorProp, overlayColor: overlayColor, shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor), surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor), elevation: ButtonStyleButton.allOrNull(elevation), padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding), minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize), fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize), maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize), side: ButtonStyleButton.allOrNull<BorderSide>(side), shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape), mouseCursor: mouseCursor, visualDensity: visualDensity, tapTargetSize: tapTargetSize, animationDuration: animationDuration, enableFeedback: enableFeedback, alignment: alignment, splashFactory: splashFactory, ); } final _FilledButtonVariant _variant; /// Defines the button's default appearance. /// /// The button [child]'s [Text] and [Icon] widgets are rendered with /// the [ButtonStyle]'s foreground color. The button's [InkWell] adds /// the style's overlay color when the button is focused, hovered /// or pressed. The button's background color becomes its [Material] /// color. /// /// All of the ButtonStyle's defaults appear below. In this list /// "Theme.foo" is shorthand for `Theme.of(context).foo`. Color /// scheme values like "onSurface(0.38)" are shorthand for /// `onSurface.withOpacity(0.38)`. [MaterialStateProperty] valued /// properties that are not followed by a sublist have the same /// value for all states, otherwise the values are as specified for /// each state, and "others" means all other states. /// /// The `textScaleFactor` is the value of /// `MediaQuery.textScaleFactorOf(context)` and the names of the /// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been /// abbreviated for readability. /// /// The color of the [ButtonStyle.textStyle] is not used, the /// [ButtonStyle.foregroundColor] color is used instead. /// /// * `textStyle` - Theme.textTheme.labelLarge /// * `backgroundColor` /// * disabled - Theme.colorScheme.onSurface(0.12) /// * others - Theme.colorScheme.secondaryContainer /// * `foregroundColor` /// * disabled - Theme.colorScheme.onSurface(0.38) /// * others - Theme.colorScheme.onSecondaryContainer /// * `overlayColor` /// * hovered - Theme.colorScheme.onSecondaryContainer(0.08) /// * focused or pressed - Theme.colorScheme.onSecondaryContainer(0.12) /// * `shadowColor` - Theme.colorScheme.shadow /// * `surfaceTintColor` - null /// * `elevation` /// * disabled - 0 /// * default - 0 /// * hovered - 1 /// * focused or pressed - 0 /// * `padding` /// * `textScaleFactor <= 1` - horizontal(16) /// * `1 < textScaleFactor <= 2` - lerp(horizontal(16), horizontal(8)) /// * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4)) /// * `3 < textScaleFactor` - horizontal(4) /// * `minimumSize` - Size(64, 40) /// * `fixedSize` - null /// * `maximumSize` - Size.infinite /// * `side` - null /// * `shape` - StadiumBorder() /// * `mouseCursor` /// * disabled - SystemMouseCursors.basic /// * others - SystemMouseCursors.click /// * `visualDensity` - Theme.visualDensity /// * `tapTargetSize` - Theme.materialTapTargetSize /// * `animationDuration` - kThemeChangeDuration /// * `enableFeedback` - true /// * `alignment` - Alignment.center /// * `splashFactory` - Theme.splashFactory /// /// The default padding values for the [FilledButton.icon] factory are slightly different: /// /// * `padding` /// * `textScaleFactor <= 1` - start(12) end(16) /// * `1 < textScaleFactor <= 2` - lerp(start(12) end(16), horizontal(8)) /// * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4)) /// * `3 < textScaleFactor` - horizontal(4) /// /// The default value for `side`, which defines the appearance of the button's /// outline, is null. That means that the outline is defined by the button /// shape's [OutlinedBorder.side]. Typically the default value of an /// [OutlinedBorder]'s side is [BorderSide.none], so an outline is not drawn. /// @override ButtonStyle defaultStyleOf(BuildContext context) { switch (_variant) { case _FilledButtonVariant.filled: return _FilledButtonDefaultsM3(context); case _FilledButtonVariant.tonal: return _FilledTonalButtonDefaultsM3(context); } } /// Returns the [FilledButtonThemeData.style] of the closest /// [FilledButtonTheme] ancestor. @override ButtonStyle? themeStyleOf(BuildContext context) { return FilledButtonTheme.of(context).style; } } EdgeInsetsGeometry _scaledPadding(BuildContext context) { return ButtonStyleButton.scaledPadding( const EdgeInsets.symmetric(horizontal: 16), const EdgeInsets.symmetric(horizontal: 8), const EdgeInsets.symmetric(horizontal: 4), MediaQuery.textScaleFactorOf(context), ); } @immutable class _FilledButtonDefaultColor extends MaterialStateProperty<Color?> with Diagnosticable { _FilledButtonDefaultColor(this.color, this.disabled); final Color? color; final Color? disabled; @override Color? resolve(Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return disabled; } return color; } } @immutable class _FilledButtonDefaultOverlay extends MaterialStateProperty<Color?> with Diagnosticable { _FilledButtonDefaultOverlay(this.overlay); final Color overlay; @override Color? resolve(Set<MaterialState> states) { if (states.contains(MaterialState.hovered)) { return overlay.withOpacity(0.08); } if (states.contains(MaterialState.focused) || states.contains(MaterialState.pressed)) { return overlay.withOpacity(0.12); } return null; } } @immutable class _FilledButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable { _FilledButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor); final MouseCursor? enabledCursor; final MouseCursor? disabledCursor; @override MouseCursor? resolve(Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return disabledCursor; } return enabledCursor; } } class _FilledButtonWithIcon extends FilledButton { _FilledButtonWithIcon({ super.key, required super.onPressed, super.onLongPress, super.onHover, super.onFocusChange, super.style, super.focusNode, bool? autofocus, Clip? clipBehavior, super.statesController, required Widget icon, required Widget label, }) : super( autofocus: autofocus ?? false, clipBehavior: clipBehavior ?? Clip.none, child: _FilledButtonWithIconChild(icon: icon, label: label) ); _FilledButtonWithIcon.tonal({ super.key, required super.onPressed, super.onLongPress, super.onHover, super.onFocusChange, super.style, super.focusNode, bool? autofocus, Clip? clipBehavior, super.statesController, required Widget icon, required Widget label, }) : super.tonal( autofocus: autofocus ?? false, clipBehavior: clipBehavior ?? Clip.none, child: _FilledButtonWithIconChild(icon: icon, label: label) ); @override ButtonStyle defaultStyleOf(BuildContext context) { final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding( const EdgeInsetsDirectional.fromSTEB(12, 0, 16, 0), const EdgeInsets.symmetric(horizontal: 8), const EdgeInsetsDirectional.fromSTEB(8, 0, 4, 0), MediaQuery.textScaleFactorOf(context), ); return super.defaultStyleOf(context).copyWith( padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(scaledPadding), ); } } class _FilledButtonWithIconChild extends StatelessWidget { const _FilledButtonWithIconChild({ required this.label, required this.icon }); final Widget label; final Widget icon; @override Widget build(BuildContext context) { final double scale = MediaQuery.textScaleFactorOf(context); // Adjust the gap based on the text scale factor. Start at 8, and lerp // to 4 based on how large the text is. final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!; return Row( mainAxisSize: MainAxisSize.min, children: <Widget>[icon, SizedBox(width: gap), Flexible(child: label)], ); } } // BEGIN GENERATED TOKEN PROPERTIES - FilledButton // Do not edit by hand. The code between the "BEGIN GENERATED" and // "END GENERATED" comments are generated from data in the Material // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. // Token database version: v0_152 class _FilledButtonDefaultsM3 extends ButtonStyle { _FilledButtonDefaultsM3(this.context) : super( animationDuration: kThemeChangeDuration, enableFeedback: true, alignment: Alignment.center, ); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; @override MaterialStateProperty<TextStyle?> get textStyle => MaterialStatePropertyAll<TextStyle?>(Theme.of(context).textTheme.labelLarge); @override MaterialStateProperty<Color?>? get backgroundColor => MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return _colors.onSurface.withOpacity(0.12); } return _colors.primary; }); @override MaterialStateProperty<Color?>? get foregroundColor => MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return _colors.onSurface.withOpacity(0.38); } return _colors.onPrimary; }); @override MaterialStateProperty<Color?>? get overlayColor => MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.hovered)) { return _colors.onPrimary.withOpacity(0.08); } if (states.contains(MaterialState.focused)) { return _colors.onPrimary.withOpacity(0.12); } if (states.contains(MaterialState.pressed)) { return _colors.onPrimary.withOpacity(0.12); } return null; }); @override MaterialStateProperty<Color>? get shadowColor => MaterialStatePropertyAll<Color>(_colors.shadow); @override MaterialStateProperty<Color>? get surfaceTintColor => const MaterialStatePropertyAll<Color>(Colors.transparent); @override MaterialStateProperty<double>? get elevation => MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return 0.0; } if (states.contains(MaterialState.hovered)) { return 1.0; } if (states.contains(MaterialState.focused)) { return 0.0; } if (states.contains(MaterialState.pressed)) { return 0.0; } return 0.0; }); @override MaterialStateProperty<EdgeInsetsGeometry>? get padding => MaterialStatePropertyAll<EdgeInsetsGeometry>(_scaledPadding(context)); @override MaterialStateProperty<Size>? get minimumSize => const MaterialStatePropertyAll<Size>(Size(64.0, 40.0)); // No default fixedSize @override MaterialStateProperty<Size>? get maximumSize => const MaterialStatePropertyAll<Size>(Size.infinite); // No default side @override MaterialStateProperty<OutlinedBorder>? get shape => const MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder()); @override MaterialStateProperty<MouseCursor?>? get mouseCursor => MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return SystemMouseCursors.basic; } return SystemMouseCursors.click; }); @override VisualDensity? get visualDensity => Theme.of(context).visualDensity; @override MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize; @override InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; } // END GENERATED TOKEN PROPERTIES - FilledButton // BEGIN GENERATED TOKEN PROPERTIES - FilledTonalButton // Do not edit by hand. The code between the "BEGIN GENERATED" and // "END GENERATED" comments are generated from data in the Material // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. // Token database version: v0_152 class _FilledTonalButtonDefaultsM3 extends ButtonStyle { _FilledTonalButtonDefaultsM3(this.context) : super( animationDuration: kThemeChangeDuration, enableFeedback: true, alignment: Alignment.center, ); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; @override MaterialStateProperty<TextStyle?> get textStyle => MaterialStatePropertyAll<TextStyle?>(Theme.of(context).textTheme.labelLarge); @override MaterialStateProperty<Color?>? get backgroundColor => MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return _colors.onSurface.withOpacity(0.12); } return _colors.secondaryContainer; }); @override MaterialStateProperty<Color?>? get foregroundColor => MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return _colors.onSurface.withOpacity(0.38); } return _colors.onSecondaryContainer; }); @override MaterialStateProperty<Color?>? get overlayColor => MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.hovered)) { return _colors.onSecondaryContainer.withOpacity(0.08); } if (states.contains(MaterialState.focused)) { return _colors.onSecondaryContainer.withOpacity(0.12); } if (states.contains(MaterialState.pressed)) { return _colors.onSecondaryContainer.withOpacity(0.12); } return null; }); @override MaterialStateProperty<Color>? get shadowColor => MaterialStatePropertyAll<Color>(_colors.shadow); @override MaterialStateProperty<Color>? get surfaceTintColor => const MaterialStatePropertyAll<Color>(Colors.transparent); @override MaterialStateProperty<double>? get elevation => MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return 0.0; } if (states.contains(MaterialState.hovered)) { return 1.0; } if (states.contains(MaterialState.focused)) { return 0.0; } if (states.contains(MaterialState.pressed)) { return 0.0; } return 0.0; }); @override MaterialStateProperty<EdgeInsetsGeometry>? get padding => MaterialStatePropertyAll<EdgeInsetsGeometry>(_scaledPadding(context)); @override MaterialStateProperty<Size>? get minimumSize => const MaterialStatePropertyAll<Size>(Size(64.0, 40.0)); // No default fixedSize @override MaterialStateProperty<Size>? get maximumSize => const MaterialStatePropertyAll<Size>(Size.infinite); // No default side @override MaterialStateProperty<OutlinedBorder>? get shape => const MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder()); @override MaterialStateProperty<MouseCursor?>? get mouseCursor => MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return SystemMouseCursors.basic; } return SystemMouseCursors.click; }); @override VisualDensity? get visualDensity => Theme.of(context).visualDensity; @override MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize; @override InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; } // END GENERATED TOKEN PROPERTIES - FilledTonalButton