Unverified Commit 2b974aed authored by Andrei Diaconu's avatar Andrei Diaconu Committed by GitHub

Make popup menus avoid display features (#98981)

parent 231c1a4b
......@@ -636,6 +636,7 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
this.selectedItemIndex,
this.textDirection,
this.padding,
this.avoidBounds,
);
// Rectangle of underlying button, relative to the overlay's dimensions.
......@@ -655,6 +656,9 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
// The padding of unsafe area.
EdgeInsets padding;
// List of rectangles that we should avoid overlapping. Unusable screen area.
final Set<Rect> avoidBounds;
// We put the child wherever position specifies, so long as it will fit within
// the specified parent size padded (inset) by 8. If necessary, we adjust the
// child's position so that it fits.
......@@ -705,19 +709,38 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
break;
}
}
final Offset wantedPosition = Offset(x, y);
final Offset originCenter = position.toRect(Offset.zero & size).center;
final Iterable<Rect> subScreens = DisplayFeatureSubScreen.subScreensInBounds(Offset.zero & size, avoidBounds);
final Rect subScreen = _closestScreen(subScreens, originCenter);
return _fitInsideScreen(subScreen, childSize, wantedPosition);
}
Rect _closestScreen(Iterable<Rect> screens, Offset point) {
Rect closest = screens.first;
for (final Rect screen in screens) {
if ((screen.center - point).distance < (closest.center - point).distance) {
closest = screen;
}
}
return closest;
}
Offset _fitInsideScreen(Rect screen, Size childSize, Offset wantedPosition){
double x = wantedPosition.dx;
double y = wantedPosition.dy;
// Avoid going outside an area defined as the rectangle 8.0 pixels from the
// edge of the screen in every direction.
if (x < _kMenuScreenPadding + padding.left)
x = _kMenuScreenPadding + padding.left;
else if (x + childSize.width > size.width - _kMenuScreenPadding - padding.right)
x = size.width - childSize.width - _kMenuScreenPadding - padding.right ;
if (y < _kMenuScreenPadding + padding.top)
if (x < screen.left + _kMenuScreenPadding + padding.left)
x = screen.left + _kMenuScreenPadding + padding.left;
else if (x + childSize.width > screen.right - _kMenuScreenPadding - padding.right)
x = screen.right - childSize.width - _kMenuScreenPadding - padding.right;
if (y < screen.top + _kMenuScreenPadding + padding.top)
y = _kMenuScreenPadding + padding.top;
else if (y + childSize.height > size.height - _kMenuScreenPadding - padding.bottom)
y = size.height - padding.bottom - _kMenuScreenPadding - childSize.height ;
else if (y + childSize.height > screen.bottom - _kMenuScreenPadding - padding.bottom)
y = screen.bottom - childSize.height - _kMenuScreenPadding - padding.bottom;
return Offset(x, y);
return Offset(x,y);
}
@override
......@@ -731,7 +754,8 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
|| selectedItemIndex != oldDelegate.selectedItemIndex
|| textDirection != oldDelegate.textDirection
|| !listEquals(itemSizes, oldDelegate.itemSizes)
|| padding != oldDelegate.padding;
|| padding != oldDelegate.padding
|| !setEquals(avoidBounds, oldDelegate.avoidBounds);
}
}
......@@ -813,6 +837,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
selectedItemIndex,
Directionality.of(context),
mediaQuery.padding,
_avoidBounds(mediaQuery),
),
child: capturedThemes.wrap(menu),
);
......@@ -820,6 +845,10 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
),
);
}
Set<Rect> _avoidBounds(MediaQueryData mediaQuery) {
return DisplayFeatureSubScreen.avoidBounds(mediaQuery).toSet();
}
}
/// Show a popup menu that contains the `items` at `position`.
......
......@@ -3,7 +3,7 @@
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' show DisplayFeature;
import 'dart:ui' show DisplayFeature, DisplayFeatureState;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
......@@ -19,9 +19,8 @@ import 'media_query.dart';
/// A [DisplayFeature] splits the screen into sub-screens when both these
/// conditions are met:
///
/// * it obstructs the screen, meaning the area it occupies is not 0. Display
/// features of type [DisplayFeatureType.fold] can have height 0 or width 0
/// and not be obstructing the screen.
/// * it obstructs the screen, meaning the area it occupies is not 0 or the
/// `state` is [DisplayFeatureState.postureHalfOpened].
/// * it is at least as tall as the screen, producing a left and right
/// sub-screen or it is at least as wide as the screen, producing a top and
/// bottom sub-screen
......@@ -100,7 +99,7 @@ class DisplayFeatureSubScreen extends StatelessWidget {
final Size parentSize = mediaQuery.size;
final Rect wantedBounds = Offset.zero & parentSize;
final Offset resolvedAnchorPoint = _capOffset(anchorPoint ?? _fallbackAnchorPoint(context), parentSize);
final Iterable<Rect> subScreens = _subScreensInBounds(wantedBounds, _avoidBounds(mediaQuery));
final Iterable<Rect> subScreens = subScreensInBounds(wantedBounds, avoidBounds(mediaQuery));
final Rect closestSubScreen = _closestToAnchorPoint(subScreens, resolvedAnchorPoint);
return Padding(
......@@ -127,9 +126,15 @@ class DisplayFeatureSubScreen extends StatelessWidget {
}
}
static Iterable<Rect> _avoidBounds(MediaQueryData mediaQuery) {
return mediaQuery.displayFeatures.map((DisplayFeature d) => d.bounds)
.where((Rect r) => r.shortestSide > 0);
/// Returns the areas of the screen that are obstructed by display features.
///
/// A [DisplayFeature] obstructs the screen when the the area it occupies is
/// not 0 or the `state` is [DisplayFeatureState.postureHalfOpened].
static Iterable<Rect> avoidBounds(MediaQueryData mediaQuery) {
return mediaQuery.displayFeatures
.where((DisplayFeature d) => d.bounds.shortestSide > 0 ||
d.state == DisplayFeatureState.postureHalfOpened)
.map((DisplayFeature d) => d.bounds);
}
/// Returns the closest sub-screen to the [anchorPoint].
......@@ -188,8 +193,8 @@ class DisplayFeatureSubScreen extends StatelessWidget {
}
/// Returns sub-screens resulted by dividing [wantedBounds] along items of
/// [avoidBounds] that are at least as high or as wide.
static Iterable<Rect> _subScreensInBounds(Rect wantedBounds, Iterable<Rect> avoidBounds) {
/// [avoidBounds] that are at least as tall or as wide.
static Iterable<Rect> subScreensInBounds(Rect wantedBounds, Iterable<Rect> avoidBounds) {
Iterable<Rect> subScreens = <Rect>[wantedBounds];
for (final Rect bounds in avoidBounds) {
final List<Rect> newSubScreens = <Rect>[];
......
......@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show SemanticsFlag;
import 'dart:ui' show SemanticsFlag, DisplayFeature, DisplayFeatureType, DisplayFeatureState;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
......@@ -853,6 +853,66 @@ void main() {
expect(tester.getTopLeft(popupFinder), buttonTopLeft);
});
testWidgets('PopupMenu positioning around display features', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: MediaQuery(
data: const MediaQueryData(
size: Size(800, 600),
displayFeatures: <DisplayFeature>[
// A 20-pixel wide vertical display feature, similar to a foldable
// with a visible hinge. Splits the display into two "virtual screens"
// and the popup menu should never overlap the display feature.
DisplayFeature(
bounds: Rect.fromLTRB(390, 0, 410, 600),
type: DisplayFeatureType.cutout,
state: DisplayFeatureState.unknown,
)
]
),
child: Scaffold(
body: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return Padding(
// Position the button in the top-right of the first "virtual screen"
padding: const EdgeInsets.only(right:390.0),
child: Align(
alignment: Alignment.topRight,
child: PopupMenuButton<int>(
key: buttonKey,
itemBuilder: (_) => <PopupMenuItem<int>>[
const PopupMenuItem<int>(value: 1, child: Text('Item 1')),
const PopupMenuItem<int>(value: 2, child: Text('Item 2')),
],
child: const Text('Show Menu'),
),
),
);
},
);
},
),
),
),
),
);
final Finder buttonFinder = find.byKey(buttonKey);
final Finder popupFinder = find.bySemanticsLabel('Popup menu');
await tester.tap(buttonFinder);
await tester.pumpAndSettle();
// Since the display feature splits the display into 2 sub-screens, popup
// menu should be positioned to fit in the first virtual screen, where the
// originating button is.
// The 8 pixels is [_kMenuScreenPadding].
expect(tester.getTopRight(popupFinder), const Offset(390 - 8, 8));
});
testWidgets('PopupMenu removes MediaQuery padding', (WidgetTester tester) async {
late BuildContext popupContext;
......
......@@ -192,6 +192,11 @@ void main() {
type: DisplayFeatureType.cutout,
state: DisplayFeatureState.unknown,
),
const DisplayFeature(
bounds: Rect.fromLTRB(0, 300, 800, 300),
type: DisplayFeatureType.fold,
state: DisplayFeatureState.postureFlat,
),
]
);
......@@ -217,5 +222,37 @@ void main() {
expect(renderBox.size.height, equals(600.0));
expect(renderBox.localToGlobal(Offset.zero), equals(Offset.zero));
});
testWidgets('with size 0 display feature in half-opened posture and anchorPoint', (WidgetTester tester) async {
const Key childKey = Key('childKey');
final MediaQueryData mediaQuery = MediaQueryData.fromWindow(WidgetsBinding.instance.window).copyWith(
displayFeatures: <DisplayFeature>[
const DisplayFeature(
bounds: Rect.fromLTRB(0, 300, 800, 300),
type: DisplayFeatureType.fold,
state: DisplayFeatureState.postureHalfOpened,
),
]
);
await tester.pumpWidget(
MediaQuery(
data: mediaQuery,
child: const DisplayFeatureSubScreen(
anchorPoint: Offset(1000, 1000),
child: SizedBox(
key: childKey,
width: double.infinity,
height: double.infinity,
),
),
),
);
final RenderBox renderBox = tester.renderObject(find.byKey(childKey));
expect(renderBox.size.width, equals(800.0));
expect(renderBox.size.height, equals(300.0));
expect(renderBox.localToGlobal(Offset.zero), equals(const Offset(0,300)));
});
});
}
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