Unverified Commit 577a88b2 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Fix MenuAnchor padding (#116573)

* Fix MenuAnchor padding

* Add tests
parent 76f03359
...@@ -1796,21 +1796,23 @@ class _SubmenuButtonState extends State<SubmenuButton> { ...@@ -1796,21 +1796,23 @@ class _SubmenuButtonState extends State<SubmenuButton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Offset menuPaddingOffset; Offset menuPaddingOffset = widget.alignmentOffset ?? Offset.zero;
final EdgeInsets menuPadding = _computeMenuPadding(context); final EdgeInsets menuPadding = _computeMenuPadding(context);
switch (_anchor?._root._orientation ?? Axis.vertical) { // Move the submenu over by the size of the menu padding, so that
// the first menu item aligns with the submenu button that opens it.
switch (_anchor?._orientation ?? Axis.vertical) {
case Axis.horizontal: case Axis.horizontal:
switch (Directionality.of(context)) { switch (Directionality.of(context)) {
case TextDirection.rtl: case TextDirection.rtl:
menuPaddingOffset = widget.alignmentOffset ?? Offset(-menuPadding.right, 0); menuPaddingOffset += Offset(menuPadding.right, 0);
break; break;
case TextDirection.ltr: case TextDirection.ltr:
menuPaddingOffset = widget.alignmentOffset ?? Offset(-menuPadding.left, 0); menuPaddingOffset += Offset(-menuPadding.left, 0);
break; break;
} }
break; break;
case Axis.vertical: case Axis.vertical:
menuPaddingOffset = widget.alignmentOffset ?? Offset(0, -menuPadding.top); menuPaddingOffset += Offset(0, -menuPadding.top);
break; break;
} }
...@@ -1900,24 +1902,13 @@ class _SubmenuButtonState extends State<SubmenuButton> { ...@@ -1900,24 +1902,13 @@ class _SubmenuButtonState extends State<SubmenuButton> {
} }
EdgeInsets _computeMenuPadding(BuildContext context) { EdgeInsets _computeMenuPadding(BuildContext context) {
final MenuStyle? themeStyle = MenuTheme.of(context).style; final MaterialStateProperty<EdgeInsetsGeometry?> insets =
final MenuStyle defaultStyle = _MenuDefaultsM3(context); widget.menuStyle?.padding ??
MenuTheme.of(context).style?.padding ??
T? effectiveValue<T>(T? Function(MenuStyle? style) getProperty) { _MenuDefaultsM3(context).padding!;
return getProperty(widget.menuStyle) ?? getProperty(themeStyle) ?? getProperty(defaultStyle); return insets
} .resolve(widget.statesController?.value ?? const <MaterialState>{})!
.resolve(Directionality.of(context));
T? resolve<T>(MaterialStateProperty<T>? Function(MenuStyle? style) getProperty) {
return effectiveValue(
(MenuStyle? style) {
return getProperty(style)?.resolve(widget.statesController?.value ?? const <MaterialState>{});
},
);
}
return resolve<EdgeInsetsGeometry?>(
(MenuStyle? style) => style?.padding,
)?.resolve(Directionality.of(context)) ?? EdgeInsets.zero;
} }
void _handleFocusChange() { void _handleFocusChange() {
...@@ -3190,14 +3181,15 @@ class _MenuLayout extends SingleChildLayoutDelegate { ...@@ -3190,14 +3181,15 @@ class _MenuLayout extends SingleChildLayoutDelegate {
@override @override
bool shouldRelayout(_MenuLayout oldDelegate) { bool shouldRelayout(_MenuLayout oldDelegate) {
return anchorRect != oldDelegate.anchorRect || return anchorRect != oldDelegate.anchorRect
textDirection != oldDelegate.textDirection || || textDirection != oldDelegate.textDirection
alignment != oldDelegate.alignment || || alignment != oldDelegate.alignment
alignmentOffset != oldDelegate.alignmentOffset || || alignmentOffset != oldDelegate.alignmentOffset
menuPosition != oldDelegate.menuPosition || || menuPosition != oldDelegate.menuPosition
orientation != oldDelegate.orientation || || menuPadding != oldDelegate.menuPadding
parentOrientation != oldDelegate.parentOrientation || || orientation != oldDelegate.orientation
!setEquals(avoidBounds, oldDelegate.avoidBounds); || parentOrientation != oldDelegate.parentOrientation
|| !setEquals(avoidBounds, oldDelegate.avoidBounds);
} }
Rect _closestScreen(Iterable<Rect> screens, Offset point) { Rect _closestScreen(Iterable<Rect> screens, Offset point) {
...@@ -3301,7 +3293,7 @@ class _MenuPanelState extends State<_MenuPanel> { ...@@ -3301,7 +3293,7 @@ class _MenuPanelState extends State<_MenuPanel> {
final double dy = densityAdjustment.dy; final double dy = densityAdjustment.dy;
final double dx = math.max(0, densityAdjustment.dx); final double dx = math.max(0, densityAdjustment.dx);
final EdgeInsetsGeometry resolvedPadding = padding final EdgeInsetsGeometry resolvedPadding = padding
.add(EdgeInsets.fromLTRB(dx, dy, dx, dy)) .add(EdgeInsets.symmetric(horizontal: dx, vertical: dy))
.clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity); // ignore_clamp_double_lint .clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity); // ignore_clamp_double_lint
BoxConstraints effectiveConstraints = visualDensity.effectiveConstraints( BoxConstraints effectiveConstraints = visualDensity.effectiveConstraints(
...@@ -3430,7 +3422,7 @@ class _Submenu extends StatelessWidget { ...@@ -3430,7 +3422,7 @@ class _Submenu extends StatelessWidget {
); );
final VisualDensity visualDensity = final VisualDensity visualDensity =
effectiveValue((MenuStyle? style) => style?.visualDensity) ?? VisualDensity.standard; effectiveValue((MenuStyle? style) => style?.visualDensity) ?? Theme.of(context).visualDensity;
final AlignmentGeometry alignment = effectiveValue((MenuStyle? style) => style?.alignment)!; final AlignmentGeometry alignment = effectiveValue((MenuStyle? style) => style?.alignment)!;
final BuildContext anchorContext = anchor._anchorKey.currentContext!; final BuildContext anchorContext = anchor._anchorKey.currentContext!;
final RenderBox overlay = Overlay.of(anchorContext).context.findRenderObject()! as RenderBox; final RenderBox overlay = Overlay.of(anchorContext).context.findRenderObject()! as RenderBox;
......
...@@ -1812,7 +1812,7 @@ void main() { ...@@ -1812,7 +1812,7 @@ void main() {
}); });
group('Layout', () { group('Layout', () {
List<Rect> collectMenuRects() { List<Rect> collectMenuItemRects() {
final List<Rect> menuRects = <Rect>[]; final List<Rect> menuRects = <Rect>[];
final List<Element> candidates = find.byType(SubmenuButton).evaluate().toList(); final List<Element> candidates = find.byType(SubmenuButton).evaluate().toList();
for (final Element candidate in candidates) { for (final Element candidate in candidates) {
...@@ -1824,6 +1824,18 @@ void main() { ...@@ -1824,6 +1824,18 @@ void main() {
return menuRects; return menuRects;
} }
List<Rect> collectSubmenuRects() {
final List<Rect> menuRects = <Rect>[];
final List<Element> candidates = findMenuPanels().evaluate().toList();
for (final Element candidate in candidates) {
final RenderBox box = candidate.renderObject! as RenderBox;
final Offset topLeft = box.localToGlobal(box.size.topLeft(Offset.zero));
final Offset bottomRight = box.localToGlobal(box.size.bottomRight(Offset.zero));
menuRects.add(Rect.fromPoints(topLeft, bottomRight));
}
return menuRects;
}
testWidgets('unconstrained menus show up in the right place in LTR', (WidgetTester tester) async { testWidgets('unconstrained menus show up in the right place in LTR', (WidgetTester tester) async {
await changeSurfaceSize(tester, const Size(800, 600)); await changeSurfaceSize(tester, const Size(800, 600));
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1855,12 +1867,16 @@ void main() { ...@@ -1855,12 +1867,16 @@ void main() {
expect(find.byType(MenuItemButton), findsNWidgets(6)); expect(find.byType(MenuItemButton), findsNWidgets(6));
expect(find.byType(SubmenuButton), findsNWidgets(5)); expect(find.byType(SubmenuButton), findsNWidgets(5));
final List<Rect> menuRects = collectMenuRects(); expect(
expect(menuRects[0], equals(const Rect.fromLTRB(4.0, 0.0, 112.0, 48.0))); collectMenuItemRects(),
expect(menuRects[1], equals(const Rect.fromLTRB(112.0, 0.0, 220.0, 48.0))); equals(const <Rect>[
expect(menuRects[2], equals(const Rect.fromLTRB(220.0, 0.0, 328.0, 48.0))); Rect.fromLTRB(4.0, 0.0, 112.0, 48.0),
expect(menuRects[3], equals(const Rect.fromLTRB(328.0, 0.0, 506.0, 48.0))); Rect.fromLTRB(112.0, 0.0, 220.0, 48.0),
expect(menuRects[4], equals(const Rect.fromLTRB(112.0, 104.0, 326.0, 152.0))); Rect.fromLTRB(220.0, 0.0, 328.0, 48.0),
Rect.fromLTRB(328.0, 0.0, 506.0, 48.0),
Rect.fromLTRB(112.0, 104.0, 326.0, 152.0),
]),
);
}); });
testWidgets('unconstrained menus show up in the right place in RTL', (WidgetTester tester) async { testWidgets('unconstrained menus show up in the right place in RTL', (WidgetTester tester) async {
...@@ -1897,12 +1913,16 @@ void main() { ...@@ -1897,12 +1913,16 @@ void main() {
expect(find.byType(MenuItemButton), findsNWidgets(6)); expect(find.byType(MenuItemButton), findsNWidgets(6));
expect(find.byType(SubmenuButton), findsNWidgets(5)); expect(find.byType(SubmenuButton), findsNWidgets(5));
final List<Rect> menuRects = collectMenuRects(); expect(
expect(menuRects[0], equals(const Rect.fromLTRB(688.0, 0.0, 796.0, 48.0))); collectMenuItemRects(),
expect(menuRects[1], equals(const Rect.fromLTRB(580.0, 0.0, 688.0, 48.0))); equals(const <Rect>[
expect(menuRects[2], equals(const Rect.fromLTRB(472.0, 0.0, 580.0, 48.0))); Rect.fromLTRB(688.0, 0.0, 796.0, 48.0),
expect(menuRects[3], equals(const Rect.fromLTRB(294.0, 0.0, 472.0, 48.0))); Rect.fromLTRB(580.0, 0.0, 688.0, 48.0),
expect(menuRects[4], equals(const Rect.fromLTRB(474.0, 104.0, 688.0, 152.0))); Rect.fromLTRB(472.0, 0.0, 580.0, 48.0),
Rect.fromLTRB(294.0, 0.0, 472.0, 48.0),
Rect.fromLTRB(474.0, 104.0, 688.0, 152.0),
]),
);
}); });
testWidgets('constrained menus show up in the right place in LTR', (WidgetTester tester) async { testWidgets('constrained menus show up in the right place in LTR', (WidgetTester tester) async {
...@@ -1937,12 +1957,16 @@ void main() { ...@@ -1937,12 +1957,16 @@ void main() {
expect(find.byType(MenuItemButton), findsNWidgets(6)); expect(find.byType(MenuItemButton), findsNWidgets(6));
expect(find.byType(SubmenuButton), findsNWidgets(5)); expect(find.byType(SubmenuButton), findsNWidgets(5));
final List<Rect> menuRects = collectMenuRects(); expect(
expect(menuRects[0], equals(const Rect.fromLTRB(4.0, 0.0, 112.0, 48.0))); collectMenuItemRects(),
expect(menuRects[1], equals(const Rect.fromLTRB(112.0, 0.0, 220.0, 48.0))); equals(const <Rect>[
expect(menuRects[2], equals(const Rect.fromLTRB(220.0, 0.0, 328.0, 48.0))); Rect.fromLTRB(4.0, 0.0, 112.0, 48.0),
expect(menuRects[3], equals(const Rect.fromLTRB(328.0, 0.0, 506.0, 48.0))); Rect.fromLTRB(112.0, 0.0, 220.0, 48.0),
expect(menuRects[4], equals(const Rect.fromLTRB(86.0, 104.0, 300.0, 152.0))); Rect.fromLTRB(220.0, 0.0, 328.0, 48.0),
Rect.fromLTRB(328.0, 0.0, 506.0, 48.0),
Rect.fromLTRB(86.0, 104.0, 300.0, 152.0),
]),
);
}); });
testWidgets('constrained menus show up in the right place in RTL', (WidgetTester tester) async { testWidgets('constrained menus show up in the right place in RTL', (WidgetTester tester) async {
...@@ -1977,12 +2001,143 @@ void main() { ...@@ -1977,12 +2001,143 @@ void main() {
expect(find.byType(MenuItemButton), findsNWidgets(6)); expect(find.byType(MenuItemButton), findsNWidgets(6));
expect(find.byType(SubmenuButton), findsNWidgets(5)); expect(find.byType(SubmenuButton), findsNWidgets(5));
final List<Rect> menuRects = collectMenuRects(); expect(
expect(menuRects[0], equals(const Rect.fromLTRB(188.0, 0.0, 296.0, 48.0))); collectMenuItemRects(),
expect(menuRects[1], equals(const Rect.fromLTRB(80.0, 0.0, 188.0, 48.0))); equals(const <Rect>[
expect(menuRects[2], equals(const Rect.fromLTRB(-28.0, 0.0, 80.0, 48.0))); Rect.fromLTRB(188.0, 0.0, 296.0, 48.0),
expect(menuRects[3], equals(const Rect.fromLTRB(-206.0, 0.0, -28.0, 48.0))); Rect.fromLTRB(80.0, 0.0, 188.0, 48.0),
expect(menuRects[4], equals(const Rect.fromLTRB(0.0, 104.0, 214.0, 152.0))); Rect.fromLTRB(-28.0, 0.0, 80.0, 48.0),
Rect.fromLTRB(-206.0, 0.0, -28.0, 48.0),
Rect.fromLTRB(0.0, 104.0, 214.0, 152.0)
]),
);
});
Future<void> buildDensityPaddingApp(WidgetTester tester, {
required TextDirection textDirection,
VisualDensity visualDensity = VisualDensity.standard,
EdgeInsetsGeometry? menuPadding,
}) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.light().copyWith(visualDensity: visualDensity),
home: Directionality(
textDirection: textDirection,
child: Material(
child: Column(
children: <Widget>[
MenuBar(
style: menuPadding != null
? MenuStyle(padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(menuPadding))
: null,
children: createTestMenus(onPressed: onPressed),
),
const Expanded(child: Placeholder()),
],
),
),
),
),
);
await tester.pump();
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
}
testWidgets('submenus account for density in LTR', (WidgetTester tester) async {
await buildDensityPaddingApp(
tester,
textDirection: TextDirection.ltr,
);
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(145.0, 0.0, 655.0, 48.0),
Rect.fromLTRB(257.0, 48.0, 471.0, 208.0),
Rect.fromLTRB(471.0, 96.0, 719.0, 304.0),
]),
);
});
testWidgets('submenus account for menu density in RTL', (WidgetTester tester) async {
await buildDensityPaddingApp(
tester,
textDirection: TextDirection.rtl,
);
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(145.0, 0.0, 655.0, 48.0),
Rect.fromLTRB(329.0, 48.0, 543.0, 208.0),
Rect.fromLTRB(81.0, 96.0, 329.0, 304.0),
]),
);
});
testWidgets('submenus account for compact menu density in LTR', (WidgetTester tester) async {
await buildDensityPaddingApp(
tester,
visualDensity: VisualDensity.compact,
textDirection: TextDirection.ltr,
);
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(145.0, 0.0, 655.0, 40.0),
Rect.fromLTRB(257.0, 40.0, 467.0, 176.0),
Rect.fromLTRB(467.0, 80.0, 715.0, 256.0),
]),
);
});
testWidgets('submenus account for compact menu density in RTL', (WidgetTester tester) async {
await buildDensityPaddingApp(
tester,
visualDensity: VisualDensity.compact,
textDirection: TextDirection.rtl,
);
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(145.0, 0.0, 655.0, 40.0),
Rect.fromLTRB(333.0, 40.0, 543.0, 176.0),
Rect.fromLTRB(85.0, 80.0, 333.0, 256.0),
]),
);
});
testWidgets('submenus account for padding in LTR', (WidgetTester tester) async {
await buildDensityPaddingApp(
tester,
menuPadding: const EdgeInsetsDirectional.only(start: 10, end: 11, top: 12, bottom: 13),
textDirection: TextDirection.ltr,
);
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(138.5, 0.0, 661.5, 73.0),
Rect.fromLTRB(256.5, 60.0, 470.5, 220.0),
Rect.fromLTRB(470.5, 108.0, 718.5, 316.0),
]),
);
});
testWidgets('submenus account for padding in RTL', (WidgetTester tester) async {
await buildDensityPaddingApp(
tester,
menuPadding: const EdgeInsetsDirectional.only(start: 10, end: 11, top: 12, bottom: 13),
textDirection: TextDirection.rtl,
);
expect(
collectSubmenuRects(),
equals(const <Rect>[
Rect.fromLTRB(138.5, 0.0, 661.5, 73.0),
Rect.fromLTRB(329.5, 60.0, 543.5, 220.0),
Rect.fromLTRB(81.5, 108.0, 329.5, 316.0),
]),
);
}); });
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment