// 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) { assert (t != null); if (a == null && b == null) { return null; } 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); } }