blob: a335eee28e5783345e09ae99c01f44b2a3c8777a [file] [log] [blame]
// 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);
}
}