1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | /// @docImport 'ink_well.dart'; |
6 | /// @docImport 'material.dart'; |
7 | /// @docImport 'menu_button_theme.dart'; |
8 | library; |
9 | |
10 | import 'dart:ui' show lerpDouble; |
11 | |
12 | import 'package:flutter/foundation.dart'; |
13 | import 'package:flutter/widgets.dart'; |
14 | |
15 | import 'button_style.dart'; |
16 | import 'material_state.dart'; |
17 | import 'menu_anchor.dart'; |
18 | import 'theme.dart'; |
19 | import 'theme_data.dart'; |
20 | |
21 | // Examples can assume: |
22 | // late Widget child; |
23 | // late BuildContext context; |
24 | // late MenuStyle style; |
25 | // @immutable |
26 | // class MyAppHome extends StatelessWidget { |
27 | // const MyAppHome({super.key}); |
28 | // @override |
29 | // Widget build(BuildContext context) => const SizedBox(); |
30 | // } |
31 | |
32 | /// The visual properties that menus have in common. |
33 | /// |
34 | /// Menus created by [MenuBar] and [MenuAnchor] and their themes have a |
35 | /// [MenuStyle] property which defines the visual properties whose default |
36 | /// values are to be overridden. The default values are defined by the |
37 | /// individual menu widgets and are typically based on overall theme's |
38 | /// [ThemeData.colorScheme] and [ThemeData.textTheme]. |
39 | /// |
40 | /// All of the [MenuStyle] properties are null by default. |
41 | /// |
42 | /// Many of the [MenuStyle] properties are [WidgetStateProperty] objects which |
43 | /// resolve to different values depending on the menu's state. For example the |
44 | /// [Color] properties are defined with `WidgetStateProperty<Color>` and can |
45 | /// resolve to different colors depending on if the menu is pressed, hovered, |
46 | /// focused, disabled, etc. |
47 | /// |
48 | /// These properties can override the default value for just one state or all of |
49 | /// them. For example to create a [SubmenuButton] whose background color is the |
50 | /// color scheme’s primary color with 50% opacity, but only when the menu is |
51 | /// pressed, one could write: |
52 | /// |
53 | /// ```dart |
54 | /// SubmenuButton( |
55 | /// menuStyle: MenuStyle( |
56 | /// backgroundColor: WidgetStateProperty.resolveWith<Color?>( |
57 | /// (Set<WidgetState> states) { |
58 | /// if (states.contains(WidgetState.focused)) { |
59 | /// return Theme.of(context).colorScheme.primary.withOpacity(0.5); |
60 | /// } |
61 | /// return null; // Use the component's default. |
62 | /// }, |
63 | /// ), |
64 | /// ), |
65 | /// menuChildren: const <Widget>[ /* ... */ ], |
66 | /// child: const Text('Fly me to the moon'), |
67 | /// ), |
68 | /// ``` |
69 | /// |
70 | /// In this case the background color for all other menu states would fall back |
71 | /// to the [SubmenuButton]'s default values. To unconditionally set the menu's |
72 | /// [backgroundColor] for all states one could write: |
73 | /// |
74 | /// ```dart |
75 | /// const SubmenuButton( |
76 | /// menuStyle: MenuStyle( |
77 | /// backgroundColor: WidgetStatePropertyAll<Color>(Colors.green), |
78 | /// ), |
79 | /// menuChildren: <Widget>[ /* ... */ ], |
80 | /// child: Text('Let me play among the stars'), |
81 | /// ), |
82 | /// ``` |
83 | /// |
84 | /// To configure all of the application's menus in the same way, specify the |
85 | /// overall theme's `menuTheme`: |
86 | /// |
87 | /// ```dart |
88 | /// MaterialApp( |
89 | /// theme: ThemeData( |
90 | /// menuTheme: const MenuThemeData( |
91 | /// style: MenuStyle(backgroundColor: WidgetStatePropertyAll<Color>(Colors.red)), |
92 | /// ), |
93 | /// ), |
94 | /// home: const MyAppHome(), |
95 | /// ), |
96 | /// ``` |
97 | /// |
98 | /// See also: |
99 | /// |
100 | /// * [MenuAnchor], a widget which hosts cascading menus. |
101 | /// * [MenuBar], a widget which defines a menu bar of buttons hosting cascading |
102 | /// menus. |
103 | /// * [MenuButtonTheme], the theme for [SubmenuButton]s and [MenuItemButton]s. |
104 | /// * [ButtonStyle], a similar configuration object for button styles. |
105 | @immutable |
106 | class MenuStyle with Diagnosticable { |
107 | /// Create a [MenuStyle]. |
108 | const MenuStyle({ |
109 | this.backgroundColor, |
110 | this.shadowColor, |
111 | this.surfaceTintColor, |
112 | this.elevation, |
113 | this.padding, |
114 | this.minimumSize, |
115 | this.fixedSize, |
116 | this.maximumSize, |
117 | this.side, |
118 | this.shape, |
119 | this.mouseCursor, |
120 | this.visualDensity, |
121 | this.alignment, |
122 | }); |
123 | |
124 | /// The menu's background fill color. |
125 | final MaterialStateProperty<Color?>? backgroundColor; |
126 | |
127 | /// The shadow color of the menu's [Material]. |
128 | /// |
129 | /// The material's elevation shadow can be difficult to see for dark themes, |
130 | /// so by default the menu classes add a semi-transparent overlay to indicate |
131 | /// elevation. See [ThemeData.applyElevationOverlayColor]. |
132 | final MaterialStateProperty<Color?>? shadowColor; |
133 | |
134 | /// The surface tint color of the menu's [Material]. |
135 | /// |
136 | /// See [Material.surfaceTintColor] for more details. |
137 | final MaterialStateProperty<Color?>? surfaceTintColor; |
138 | |
139 | /// The elevation of the menu's [Material]. |
140 | final MaterialStateProperty<double?>? elevation; |
141 | |
142 | /// The padding between the menu's boundary and its child. |
143 | final MaterialStateProperty<EdgeInsetsGeometry?>? padding; |
144 | |
145 | /// The minimum size of the menu itself. |
146 | /// |
147 | /// This value must be less than or equal to [maximumSize]. |
148 | final MaterialStateProperty<Size?>? minimumSize; |
149 | |
150 | /// The menu's size. |
151 | /// |
152 | /// This size is still constrained by the style's [minimumSize] and |
153 | /// [maximumSize]. Fixed size dimensions whose value is [double.infinity] are |
154 | /// ignored. |
155 | /// |
156 | /// To specify menus with a fixed width and the default height use `fixedSize: |
157 | /// Size.fromWidth(320)`. Similarly, to specify a fixed height and the default |
158 | /// width use `fixedSize: Size.fromHeight(100)`. |
159 | final MaterialStateProperty<Size?>? fixedSize; |
160 | |
161 | /// The maximum size of the menu itself. |
162 | /// |
163 | /// A [Size.infinite] or null value for this property means that the menu's |
164 | /// maximum size is not constrained. |
165 | /// |
166 | /// This value must be greater than or equal to [minimumSize]. |
167 | final MaterialStateProperty<Size?>? maximumSize; |
168 | |
169 | /// The color and weight of the menu's outline. |
170 | /// |
171 | /// This value is combined with [shape] to create a shape decorated with an |
172 | /// outline. |
173 | final MaterialStateProperty<BorderSide?>? side; |
174 | |
175 | /// The shape of the menu's underlying [Material]. |
176 | /// |
177 | /// This shape is combined with [side] to create a shape decorated with an |
178 | /// outline. |
179 | final MaterialStateProperty<OutlinedBorder?>? shape; |
180 | |
181 | /// The cursor for a mouse pointer when it enters or is hovering over this |
182 | /// menu's [InkWell]. |
183 | final MaterialStateProperty<MouseCursor?>? mouseCursor; |
184 | |
185 | /// Defines how compact the menu's layout will be. |
186 | /// |
187 | /// {@macro flutter.material.themedata.visualDensity} |
188 | /// |
189 | /// See also: |
190 | /// |
191 | /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all |
192 | /// widgets within a [Theme]. |
193 | final VisualDensity? visualDensity; |
194 | |
195 | /// Determines the desired alignment of the submenu when opened relative to |
196 | /// the button that opens it. |
197 | /// |
198 | /// If there isn't sufficient space to open the menu with the given alignment, |
199 | /// and there's space on the other side of the button, then the alignment is |
200 | /// swapped to it's opposite (1 becomes -1, etc.), and the menu will try to |
201 | /// appear on the other side of the button. If there isn't enough space there |
202 | /// either, then the menu will be pushed as far over as necessary to display |
203 | /// as much of itself as possible, possibly overlapping the parent button. |
204 | final AlignmentGeometry? alignment; |
205 | |
206 | @override |
207 | int get hashCode { |
208 | final List<Object?> values = <Object?>[ |
209 | backgroundColor, |
210 | shadowColor, |
211 | surfaceTintColor, |
212 | elevation, |
213 | padding, |
214 | minimumSize, |
215 | fixedSize, |
216 | maximumSize, |
217 | side, |
218 | shape, |
219 | mouseCursor, |
220 | visualDensity, |
221 | alignment, |
222 | ]; |
223 | return Object.hashAll(values); |
224 | } |
225 | |
226 | @override |
227 | bool operator ==(Object other) { |
228 | if (identical(this, other)) { |
229 | return true; |
230 | } |
231 | if (other.runtimeType != runtimeType) { |
232 | return false; |
233 | } |
234 | return other is MenuStyle |
235 | && other.backgroundColor == backgroundColor |
236 | && other.shadowColor == shadowColor |
237 | && other.surfaceTintColor == surfaceTintColor |
238 | && other.elevation == elevation |
239 | && other.padding == padding |
240 | && other.minimumSize == minimumSize |
241 | && other.fixedSize == fixedSize |
242 | && other.maximumSize == maximumSize |
243 | && other.side == side |
244 | && other.shape == shape |
245 | && other.mouseCursor == mouseCursor |
246 | && other.visualDensity == visualDensity |
247 | && other.alignment == alignment; |
248 | } |
249 | |
250 | /// Returns a copy of this MenuStyle with the given fields replaced with |
251 | /// the new values. |
252 | MenuStyle copyWith({ |
253 | MaterialStateProperty<Color?>? backgroundColor, |
254 | MaterialStateProperty<Color?>? shadowColor, |
255 | MaterialStateProperty<Color?>? surfaceTintColor, |
256 | MaterialStateProperty<double?>? elevation, |
257 | MaterialStateProperty<EdgeInsetsGeometry?>? padding, |
258 | MaterialStateProperty<Size?>? minimumSize, |
259 | MaterialStateProperty<Size?>? fixedSize, |
260 | MaterialStateProperty<Size?>? maximumSize, |
261 | MaterialStateProperty<BorderSide?>? side, |
262 | MaterialStateProperty<OutlinedBorder?>? shape, |
263 | MaterialStateProperty<MouseCursor?>? mouseCursor, |
264 | VisualDensity? visualDensity, |
265 | AlignmentGeometry? alignment, |
266 | }) { |
267 | return MenuStyle( |
268 | backgroundColor: backgroundColor ?? this.backgroundColor, |
269 | shadowColor: shadowColor ?? this.shadowColor, |
270 | surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor, |
271 | elevation: elevation ?? this.elevation, |
272 | padding: padding ?? this.padding, |
273 | minimumSize: minimumSize ?? this.minimumSize, |
274 | fixedSize: fixedSize ?? this.fixedSize, |
275 | maximumSize: maximumSize ?? this.maximumSize, |
276 | side: side ?? this.side, |
277 | shape: shape ?? this.shape, |
278 | mouseCursor: mouseCursor ?? this.mouseCursor, |
279 | visualDensity: visualDensity ?? this.visualDensity, |
280 | alignment: alignment ?? this.alignment, |
281 | ); |
282 | } |
283 | |
284 | /// Returns a copy of this MenuStyle where the non-null fields in [style] |
285 | /// have replaced the corresponding null fields in this MenuStyle. |
286 | /// |
287 | /// In other words, [style] is used to fill in unspecified (null) fields |
288 | /// this MenuStyle. |
289 | MenuStyle merge(MenuStyle? style) { |
290 | if (style == null) { |
291 | return this; |
292 | } |
293 | return copyWith( |
294 | backgroundColor: backgroundColor ?? style.backgroundColor, |
295 | shadowColor: shadowColor ?? style.shadowColor, |
296 | surfaceTintColor: surfaceTintColor ?? style.surfaceTintColor, |
297 | elevation: elevation ?? style.elevation, |
298 | padding: padding ?? style.padding, |
299 | minimumSize: minimumSize ?? style.minimumSize, |
300 | fixedSize: fixedSize ?? style.fixedSize, |
301 | maximumSize: maximumSize ?? style.maximumSize, |
302 | side: side ?? style.side, |
303 | shape: shape ?? style.shape, |
304 | mouseCursor: mouseCursor ?? style.mouseCursor, |
305 | visualDensity: visualDensity ?? style.visualDensity, |
306 | alignment: alignment ?? style.alignment, |
307 | ); |
308 | } |
309 | |
310 | /// Linearly interpolate between two [MenuStyle]s. |
311 | static MenuStyle? lerp(MenuStyle? a, MenuStyle? b, double t) { |
312 | if (identical(a, b)) { |
313 | return a; |
314 | } |
315 | return MenuStyle( |
316 | backgroundColor: MaterialStateProperty.lerp<Color?>(a?.backgroundColor, b?.backgroundColor, t, Color.lerp), |
317 | shadowColor: MaterialStateProperty.lerp<Color?>(a?.shadowColor, b?.shadowColor, t, Color.lerp), |
318 | surfaceTintColor: MaterialStateProperty.lerp<Color?>(a?.surfaceTintColor, b?.surfaceTintColor, t, Color.lerp), |
319 | elevation: MaterialStateProperty.lerp<double?>(a?.elevation, b?.elevation, t, lerpDouble), |
320 | padding: MaterialStateProperty.lerp<EdgeInsetsGeometry?>(a?.padding, b?.padding, t, EdgeInsetsGeometry.lerp), |
321 | minimumSize: MaterialStateProperty.lerp<Size?>(a?.minimumSize, b?.minimumSize, t, Size.lerp), |
322 | fixedSize: MaterialStateProperty.lerp<Size?>(a?.fixedSize, b?.fixedSize, t, Size.lerp), |
323 | maximumSize: MaterialStateProperty.lerp<Size?>(a?.maximumSize, b?.maximumSize, t, Size.lerp), |
324 | side: MaterialStateBorderSide.lerp(a?.side, b?.side, t), |
325 | shape: MaterialStateProperty.lerp<OutlinedBorder?>(a?.shape, b?.shape, t, OutlinedBorder.lerp), |
326 | mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor, |
327 | visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity, |
328 | alignment: AlignmentGeometry.lerp(a?.alignment, b?.alignment, t), |
329 | ); |
330 | } |
331 | |
332 | @override |
333 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
334 | super.debugFillProperties(properties); |
335 | properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('backgroundColor' , backgroundColor, defaultValue: null)); |
336 | properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('shadowColor' , shadowColor, defaultValue: null)); |
337 | properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('surfaceTintColor' , surfaceTintColor, defaultValue: null)); |
338 | properties.add(DiagnosticsProperty<MaterialStateProperty<double?>>('elevation' , elevation, defaultValue: null)); |
339 | properties.add(DiagnosticsProperty<MaterialStateProperty<EdgeInsetsGeometry?>>('padding' , padding, defaultValue: null)); |
340 | properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('minimumSize' , minimumSize, defaultValue: null)); |
341 | properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('fixedSize' , fixedSize, defaultValue: null)); |
342 | properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('maximumSize' , maximumSize, defaultValue: null)); |
343 | properties.add(DiagnosticsProperty<MaterialStateProperty<BorderSide?>>('side' , side, defaultValue: null)); |
344 | properties.add(DiagnosticsProperty<MaterialStateProperty<OutlinedBorder?>>('shape' , shape, defaultValue: null)); |
345 | properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor' , mouseCursor, defaultValue: null)); |
346 | properties.add(DiagnosticsProperty<VisualDensity>('visualDensity' , visualDensity, defaultValue: null)); |
347 | properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment' , alignment, defaultValue: null)); |
348 | } |
349 | } |
350 | |