| // 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:ui' show lerpDouble; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'button_style.dart'; |
| import 'material_state.dart'; |
| import 'menu_anchor.dart'; |
| import 'theme.dart'; |
| import 'theme_data.dart'; |
| |
| // Examples can assume: |
| // late Widget child; |
| // late BuildContext context; |
| // late MenuStyle style; |
| // @immutable |
| // class MyAppHome extends StatelessWidget { |
| // const MyAppHome({super.key}); |
| // @override |
| // Widget build(BuildContext context) => const SizedBox(); |
| // } |
| |
| /// The visual properties that menus have in common. |
| /// |
| /// Menus created by [MenuBar] and [MenuAnchor] and their themes have a |
| /// [MenuStyle] property which defines the visual properties whose default |
| /// values are to be overridden. The default values are defined by the |
| /// individual menu widgets and are typically based on overall theme's |
| /// [ThemeData.colorScheme] and [ThemeData.textTheme]. |
| /// |
| /// All of the [MenuStyle] properties are null by default. |
| /// |
| /// Many of the [MenuStyle] properties are [MaterialStateProperty] objects which |
| /// resolve to different values depending on the menu's state. For example the |
| /// [Color] properties are defined with `MaterialStateProperty<Color>` and can |
| /// resolve to different colors depending on if the menu is pressed, hovered, |
| /// focused, disabled, etc. |
| /// |
| /// These properties can override the default value for just one state or all of |
| /// them. For example to create a [SubmenuButton] whose background color is the |
| /// color scheme’s primary color with 50% opacity, but only when the menu is |
| /// pressed, one could write: |
| /// |
| /// ```dart |
| /// SubmenuButton( |
| /// menuStyle: MenuStyle( |
| /// backgroundColor: MaterialStateProperty.resolveWith<Color?>( |
| /// (Set<MaterialState> states) { |
| /// if (states.contains(MaterialState.focused)) { |
| /// return Theme.of(context).colorScheme.primary.withOpacity(0.5); |
| /// } |
| /// return null; // Use the component's default. |
| /// }, |
| /// ), |
| /// ), |
| /// menuChildren: const <Widget>[ /* ... */ ], |
| /// child: const Text('Fly me to the moon'), |
| /// ), |
| /// ``` |
| /// |
| /// In this case the background color for all other menu states would fall back |
| /// to the [SubmenuButton]'s default values. To unconditionally set the menu's |
| /// [backgroundColor] for all states one could write: |
| /// |
| /// ```dart |
| /// const SubmenuButton( |
| /// menuStyle: MenuStyle( |
| /// backgroundColor: MaterialStatePropertyAll<Color>(Colors.green), |
| /// ), |
| /// menuChildren: <Widget>[ /* ... */ ], |
| /// child: Text('Let me play among the stars'), |
| /// ), |
| /// ``` |
| /// |
| /// To configure all of the application's menus in the same way, specify the |
| /// overall theme's `menuTheme`: |
| /// |
| /// ```dart |
| /// MaterialApp( |
| /// theme: ThemeData( |
| /// menuTheme: const MenuThemeData( |
| /// style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.red)), |
| /// ), |
| /// ), |
| /// home: const MyAppHome(), |
| /// ), |
| /// ``` |
| /// |
| /// See also: |
| /// |
| /// * [MenuAnchor], a widget which hosts cascading menus. |
| /// * [MenuBar], a widget which defines a menu bar of buttons hosting cascading |
| /// menus. |
| /// * [MenuButtonTheme], the theme for [SubmenuButton]s and [MenuItemButton]s. |
| /// * [ButtonStyle], a similar configuration object for button styles. |
| @immutable |
| class MenuStyle with Diagnosticable { |
| /// Create a [MenuStyle]. |
| const MenuStyle({ |
| this.backgroundColor, |
| this.shadowColor, |
| this.surfaceTintColor, |
| this.elevation, |
| this.padding, |
| this.minimumSize, |
| this.fixedSize, |
| this.maximumSize, |
| this.side, |
| this.shape, |
| this.mouseCursor, |
| this.visualDensity, |
| this.alignment, |
| }); |
| |
| /// The menu's background fill color. |
| final MaterialStateProperty<Color?>? backgroundColor; |
| |
| /// The shadow color of the menu's [Material]. |
| /// |
| /// The material's elevation shadow can be difficult to see for dark themes, |
| /// so by default the menu classes add a semi-transparent overlay to indicate |
| /// elevation. See [ThemeData.applyElevationOverlayColor]. |
| final MaterialStateProperty<Color?>? shadowColor; |
| |
| /// The surface tint color of the menu's [Material]. |
| /// |
| /// See [Material.surfaceTintColor] for more details. |
| final MaterialStateProperty<Color?>? surfaceTintColor; |
| |
| /// The elevation of the menu's [Material]. |
| final MaterialStateProperty<double?>? elevation; |
| |
| /// The padding between the menu's boundary and its child. |
| final MaterialStateProperty<EdgeInsetsGeometry?>? padding; |
| |
| /// The minimum size of the menu itself. |
| /// |
| /// This value must be less than or equal to [maximumSize]. |
| final MaterialStateProperty<Size?>? minimumSize; |
| |
| /// The menu's size. |
| /// |
| /// This size is still constrained by the style's [minimumSize] and |
| /// [maximumSize]. Fixed size dimensions whose value is [double.infinity] are |
| /// ignored. |
| /// |
| /// To specify menus with a fixed width and the default height use `fixedSize: |
| /// Size.fromWidth(320)`. Similarly, to specify a fixed height and the default |
| /// width use `fixedSize: Size.fromHeight(100)`. |
| final MaterialStateProperty<Size?>? fixedSize; |
| |
| /// The maximum size of the menu itself. |
| /// |
| /// A [Size.infinite] or null value for this property means that the menu's |
| /// maximum size is not constrained. |
| /// |
| /// This value must be greater than or equal to [minimumSize]. |
| final MaterialStateProperty<Size?>? maximumSize; |
| |
| /// The color and weight of the menu's outline. |
| /// |
| /// This value is combined with [shape] to create a shape decorated with an |
| /// outline. |
| final MaterialStateProperty<BorderSide?>? side; |
| |
| /// The shape of the menu's underlying [Material]. |
| /// |
| /// This shape is combined with [side] to create a shape decorated with an |
| /// outline. |
| final MaterialStateProperty<OutlinedBorder?>? shape; |
| |
| /// The cursor for a mouse pointer when it enters or is hovering over this |
| /// menu's [InkWell]. |
| final MaterialStateProperty<MouseCursor?>? mouseCursor; |
| |
| /// Defines how compact the menu's layout will be. |
| /// |
| /// {@macro flutter.material.themedata.visualDensity} |
| /// |
| /// See also: |
| /// |
| /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all |
| /// widgets within a [Theme]. |
| final VisualDensity? visualDensity; |
| |
| /// Determines the desired alignment of the submenu when opened relative to |
| /// the button that opens it. |
| /// |
| /// If there isn't sufficient space to open the menu with the given alignment, |
| /// and there's space on the other side of the button, then the alignment is |
| /// swapped to it's opposite (1 becomes -1, etc.), and the menu will try to |
| /// appear on the other side of the button. If there isn't enough space there |
| /// either, then the menu will be pushed as far over as necessary to display |
| /// as much of itself as possible, possibly overlapping the parent button. |
| final AlignmentGeometry? alignment; |
| |
| @override |
| int get hashCode { |
| final List<Object?> values = <Object?>[ |
| backgroundColor, |
| shadowColor, |
| surfaceTintColor, |
| elevation, |
| padding, |
| minimumSize, |
| fixedSize, |
| maximumSize, |
| side, |
| shape, |
| mouseCursor, |
| visualDensity, |
| alignment, |
| ]; |
| return Object.hashAll(values); |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (identical(this, other)) { |
| return true; |
| } |
| if (other.runtimeType != runtimeType) { |
| return false; |
| } |
| return other is MenuStyle |
| && other.backgroundColor == backgroundColor |
| && other.shadowColor == shadowColor |
| && other.surfaceTintColor == surfaceTintColor |
| && other.elevation == elevation |
| && other.padding == padding |
| && other.minimumSize == minimumSize |
| && other.fixedSize == fixedSize |
| && other.maximumSize == maximumSize |
| && other.side == side |
| && other.shape == shape |
| && other.mouseCursor == mouseCursor |
| && other.visualDensity == visualDensity |
| && other.alignment == alignment; |
| } |
| |
| /// Returns a copy of this MenuStyle with the given fields replaced with |
| /// the new values. |
| MenuStyle copyWith({ |
| MaterialStateProperty<Color?>? backgroundColor, |
| MaterialStateProperty<Color?>? shadowColor, |
| MaterialStateProperty<Color?>? surfaceTintColor, |
| MaterialStateProperty<double?>? elevation, |
| MaterialStateProperty<EdgeInsetsGeometry?>? padding, |
| MaterialStateProperty<Size?>? minimumSize, |
| MaterialStateProperty<Size?>? fixedSize, |
| MaterialStateProperty<Size?>? maximumSize, |
| MaterialStateProperty<BorderSide?>? side, |
| MaterialStateProperty<OutlinedBorder?>? shape, |
| MaterialStateProperty<MouseCursor?>? mouseCursor, |
| VisualDensity? visualDensity, |
| AlignmentGeometry? alignment, |
| }) { |
| return MenuStyle( |
| backgroundColor: backgroundColor ?? this.backgroundColor, |
| shadowColor: shadowColor ?? this.shadowColor, |
| surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor, |
| elevation: elevation ?? this.elevation, |
| padding: padding ?? this.padding, |
| minimumSize: minimumSize ?? this.minimumSize, |
| fixedSize: fixedSize ?? this.fixedSize, |
| maximumSize: maximumSize ?? this.maximumSize, |
| side: side ?? this.side, |
| shape: shape ?? this.shape, |
| mouseCursor: mouseCursor ?? this.mouseCursor, |
| visualDensity: visualDensity ?? this.visualDensity, |
| alignment: alignment ?? this.alignment, |
| ); |
| } |
| |
| /// Returns a copy of this MenuStyle where the non-null fields in [style] |
| /// have replaced the corresponding null fields in this MenuStyle. |
| /// |
| /// In other words, [style] is used to fill in unspecified (null) fields |
| /// this MenuStyle. |
| MenuStyle merge(MenuStyle? style) { |
| if (style == null) { |
| return this; |
| } |
| return copyWith( |
| backgroundColor: backgroundColor ?? style.backgroundColor, |
| shadowColor: shadowColor ?? style.shadowColor, |
| surfaceTintColor: surfaceTintColor ?? style.surfaceTintColor, |
| elevation: elevation ?? style.elevation, |
| padding: padding ?? style.padding, |
| minimumSize: minimumSize ?? style.minimumSize, |
| fixedSize: fixedSize ?? style.fixedSize, |
| maximumSize: maximumSize ?? style.maximumSize, |
| side: side ?? style.side, |
| shape: shape ?? style.shape, |
| mouseCursor: mouseCursor ?? style.mouseCursor, |
| visualDensity: visualDensity ?? style.visualDensity, |
| alignment: alignment ?? style.alignment, |
| ); |
| } |
| |
| /// Linearly interpolate between two [MenuStyle]s. |
| static MenuStyle? lerp(MenuStyle? a, MenuStyle? b, double t) { |
| if (identical(a, b)) { |
| return a; |
| } |
| return MenuStyle( |
| backgroundColor: MaterialStateProperty.lerp<Color?>(a?.backgroundColor, b?.backgroundColor, t, Color.lerp), |
| shadowColor: MaterialStateProperty.lerp<Color?>(a?.shadowColor, b?.shadowColor, t, Color.lerp), |
| surfaceTintColor: MaterialStateProperty.lerp<Color?>(a?.surfaceTintColor, b?.surfaceTintColor, t, Color.lerp), |
| elevation: MaterialStateProperty.lerp<double?>(a?.elevation, b?.elevation, t, lerpDouble), |
| padding: MaterialStateProperty.lerp<EdgeInsetsGeometry?>(a?.padding, b?.padding, t, EdgeInsetsGeometry.lerp), |
| minimumSize: MaterialStateProperty.lerp<Size?>(a?.minimumSize, b?.minimumSize, t, Size.lerp), |
| fixedSize: MaterialStateProperty.lerp<Size?>(a?.fixedSize, b?.fixedSize, t, Size.lerp), |
| maximumSize: MaterialStateProperty.lerp<Size?>(a?.maximumSize, b?.maximumSize, t, Size.lerp), |
| side: _LerpSides(a?.side, b?.side, t), |
| shape: MaterialStateProperty.lerp<OutlinedBorder?>(a?.shape, b?.shape, t, OutlinedBorder.lerp), |
| mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor, |
| visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity, |
| alignment: AlignmentGeometry.lerp(a?.alignment, b?.alignment, t), |
| ); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('backgroundColor', backgroundColor, defaultValue: null)); |
| properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('shadowColor', shadowColor, defaultValue: null)); |
| properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('surfaceTintColor', surfaceTintColor, defaultValue: null)); |
| properties.add(DiagnosticsProperty<MaterialStateProperty<double?>>('elevation', elevation, defaultValue: null)); |
| properties.add(DiagnosticsProperty<MaterialStateProperty<EdgeInsetsGeometry?>>('padding', padding, defaultValue: null)); |
| properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('minimumSize', minimumSize, defaultValue: null)); |
| properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('fixedSize', fixedSize, defaultValue: null)); |
| properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('maximumSize', maximumSize, defaultValue: null)); |
| properties.add(DiagnosticsProperty<MaterialStateProperty<BorderSide?>>('side', side, defaultValue: null)); |
| properties.add(DiagnosticsProperty<MaterialStateProperty<OutlinedBorder?>>('shape', shape, defaultValue: null)); |
| properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null)); |
| properties.add(DiagnosticsProperty<VisualDensity>('visualDensity', visualDensity, defaultValue: null)); |
| properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null)); |
| } |
| } |
| |
| /// A required helper class because [BorderSide.lerp] doesn't support passing or |
| /// returning null values. |
| class _LerpSides implements MaterialStateProperty<BorderSide?> { |
| const _LerpSides(this.a, this.b, this.t); |
| |
| final MaterialStateProperty<BorderSide?>? a; |
| final MaterialStateProperty<BorderSide?>? b; |
| final double t; |
| |
| @override |
| BorderSide? resolve(Set<MaterialState> states) { |
| final BorderSide? resolvedA = a?.resolve(states); |
| final BorderSide? resolvedB = b?.resolve(states); |
| if (resolvedA == null && resolvedB == null) { |
| return null; |
| } |
| if (resolvedA == null) { |
| return BorderSide.lerp(BorderSide(width: 0, color: resolvedB!.color.withAlpha(0)), resolvedB, t); |
| } |
| if (resolvedB == null) { |
| return BorderSide.lerp(resolvedA, BorderSide(width: 0, color: resolvedA.color.withAlpha(0)), t); |
| } |
| return BorderSide.lerp(resolvedA, resolvedB, t); |
| } |
| } |