Unverified Commit 41622725 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Overlay supports unconstrained environments (#139513)

Fixes https://github.com/flutter/flutter/issues/137875.

Unfortunately, we cannot auto-detect which OverlayEntry should be sizing the Overlay in unconstrained environment. So, this PR adds a special flag to annotate the Overlay Entry that should be used.
parent cea2726b
...@@ -82,6 +82,7 @@ class OverlayEntry implements Listenable { ...@@ -82,6 +82,7 @@ class OverlayEntry implements Listenable {
required this.builder, required this.builder,
bool opaque = false, bool opaque = false,
bool maintainState = false, bool maintainState = false,
this.canSizeOverlay = false,
}) : _opaque = opaque, }) : _opaque = opaque,
_maintainState = maintainState { _maintainState = maintainState {
if (kFlutterMemoryAllocationsEnabled) { if (kFlutterMemoryAllocationsEnabled) {
...@@ -138,6 +139,25 @@ class OverlayEntry implements Listenable { ...@@ -138,6 +139,25 @@ class OverlayEntry implements Listenable {
_overlay!._didChangeEntryOpacity(); _overlay!._didChangeEntryOpacity();
} }
/// Whether the content of this [OverlayEntry] can be used to size the
/// [Overlay].
///
/// In most situations the overlay sizes itself based on its incoming
/// constraints to be as large as possible. However, if that would result in
/// an infinite size, it has to rely on one of its children to size itself. In
/// this situation, the overlay will consult the topmost non-[Positioned]
/// overlay entry that has this property set to true, lay it out with the
/// incoming [BoxConstraints] of the overlay, and force all other
/// non-[Positioned] overlay entries to have the same size. The [Positioned]
/// entries are laid out as usual based on the calculated size of the overlay.
///
/// Overlay entries that set this to true must be able to handle unconstrained
/// [BoxConstraints].
///
/// Setting this to true has no effect if the overlay entry uses a [Positioned]
/// widget to position itself in the overlay.
final bool canSizeOverlay;
/// Whether the [OverlayEntry] is currently mounted in the widget tree. /// Whether the [OverlayEntry] is currently mounted in the widget tree.
/// ///
/// The [OverlayEntry] notifies its listeners when this value changes. /// The [OverlayEntry] notifies its listeners when this value changes.
...@@ -830,6 +850,7 @@ class _WrappingOverlay extends StatefulWidget { ...@@ -830,6 +850,7 @@ class _WrappingOverlay extends StatefulWidget {
class _WrappingOverlayState extends State<_WrappingOverlay> { class _WrappingOverlayState extends State<_WrappingOverlay> {
late final OverlayEntry _entry = OverlayEntry( late final OverlayEntry _entry = OverlayEntry(
canSizeOverlay: true,
opaque: true, opaque: true,
builder: (BuildContext context) { builder: (BuildContext context) {
return widget.child; return widget.child;
...@@ -940,29 +961,18 @@ mixin _RenderTheaterMixin on RenderBox { ...@@ -940,29 +961,18 @@ mixin _RenderTheaterMixin on RenderBox {
} }
} }
@override void layoutChild(RenderBox child, BoxConstraints nonPositionedChildConstraints) {
bool get sizedByParent => true;
@override
void performLayout() {
final Iterator<RenderBox> iterator = _childrenInPaintOrder().iterator;
// Same BoxConstraints as used by RenderStack for StackFit.expand.
final BoxConstraints nonPositionedChildConstraints = BoxConstraints.tight(constraints.biggest);
final Alignment alignment = theater._resolvedAlignment;
while (iterator.moveNext()) {
final RenderBox child = iterator.current;
final StackParentData childParentData = child.parentData! as StackParentData; final StackParentData childParentData = child.parentData! as StackParentData;
final Alignment alignment = theater._resolvedAlignment;
if (!childParentData.isPositioned) { if (!childParentData.isPositioned) {
child.layout(nonPositionedChildConstraints, parentUsesSize: true); child.layout(nonPositionedChildConstraints, parentUsesSize: true);
childParentData.offset = alignment.alongOffset(size - child.size as Offset); childParentData.offset = Offset.zero;
} else { } else {
assert(child is! _RenderDeferredLayoutBox, 'all _RenderDeferredLayoutBoxes must be non-positioned children.'); assert(child is! _RenderDeferredLayoutBox, 'all _RenderDeferredLayoutBoxes must be non-positioned children.');
RenderStack.layoutPositionedChild(child, childParentData, size, alignment); RenderStack.layoutPositionedChild(child, childParentData, size, alignment);
} }
assert(child.parentData == childParentData); assert(child.parentData == childParentData);
} }
}
@override @override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
...@@ -1208,9 +1218,11 @@ class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox ...@@ -1208,9 +1218,11 @@ class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox
@override @override
Size computeDryLayout(BoxConstraints constraints) { Size computeDryLayout(BoxConstraints constraints) {
assert(constraints.biggest.isFinite); if (constraints.biggest.isFinite) {
return constraints.biggest; return constraints.biggest;
} }
return _findSizeDeterminingChild().getDryLayout(constraints);
}
@override @override
// The following uses sync* because concurrent modifications should be allowed // The following uses sync* because concurrent modifications should be allowed
...@@ -1249,6 +1261,53 @@ class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox ...@@ -1249,6 +1261,53 @@ class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox
} }
} }
@override
bool get sizedByParent => false;
@override
void performLayout() {
RenderBox? sizeDeterminingChild;
if (constraints.biggest.isFinite) {
size = constraints.biggest;
} else {
sizeDeterminingChild = _findSizeDeterminingChild();
layoutChild(sizeDeterminingChild, constraints);
size = sizeDeterminingChild.size;
}
// Equivalent to BoxConstraints used by RenderStack for StackFit.expand.
final BoxConstraints nonPositionedChildConstraints = BoxConstraints.tight(size);
for (final RenderBox child in _childrenInPaintOrder()) {
if (child != sizeDeterminingChild) {
layoutChild(child, nonPositionedChildConstraints);
}
}
}
RenderBox _findSizeDeterminingChild() {
RenderBox? child = _lastOnstageChild;
while (child != null) {
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
if ((childParentData.overlayEntry?.canSizeOverlay ?? false) && !childParentData.isPositioned) {
return child;
}
child = childParentData.previousSibling;
}
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Overlay was given infinite constraints and cannot be sized by a suitable child.'),
ErrorDescription(
'The constraints given to the overlay ($constraints) would result in an illegal '
'infinite size (${constraints.biggest}). To avoid that, the Overlay tried to size '
'itself to one of its children, but no suitable non-positioned child that belongs to an '
'OverlayEntry with canSizeOverlay set to true could be found.',
),
ErrorHint(
'Try wrapping the Overlay in a SizedBox to give it a finite size or '
'use an OverlayEntry with canSizeOverlay set to true.',
),
]);
}
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override @override
...@@ -2185,6 +2244,9 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterM ...@@ -2185,6 +2244,9 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterM
_callingMarkParentNeedsLayout = false; _callingMarkParentNeedsLayout = false;
} }
@override
bool get sizedByParent => true;
bool _needsLayout = true; bool _needsLayout = true;
@override @override
void markNeedsLayout() { void markNeedsLayout() {
...@@ -2254,7 +2316,8 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterM ...@@ -2254,7 +2316,8 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterM
_needsLayout = false; _needsLayout = false;
return; return;
} }
super.performLayout(); assert(constraints.isTight);
layoutChild(child, constraints);
assert(() { assert(() {
_debugMutationsLocked = false; _debugMutationsLocked = false;
return true; return true;
......
...@@ -1847,7 +1847,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1847,7 +1847,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
Iterable<OverlayEntry> createOverlayEntries() { Iterable<OverlayEntry> createOverlayEntries() {
return <OverlayEntry>[ return <OverlayEntry>[
_modalBarrier = OverlayEntry(builder: _buildModalBarrier), _modalBarrier = OverlayEntry(builder: _buildModalBarrier),
_modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState), _modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState, canSizeOverlay: opaque),
]; ];
} }
......
...@@ -1602,6 +1602,19 @@ void main() { ...@@ -1602,6 +1602,19 @@ void main() {
expect(find.byType(AnimatedTheme), findsNothing); expect(find.byType(AnimatedTheme), findsNothing);
expect(find.byType(Theme), findsOneWidget); expect(find.byType(Theme), findsOneWidget);
}); });
// Regression test for https://github.com/flutter/flutter/issues/137875.
testWidgets('MaterialApp works in an unconstrained environment', (WidgetTester tester) async {
await tester.pumpWidget(
const UnconstrainedBox(
child: MaterialApp(
home: SizedBox(width: 123, height: 456),
),
),
);
expect(tester.getSize(find.byType(MaterialApp)), const Size(123, 456));
});
} }
class MockScrollBehavior extends ScrollBehavior { class MockScrollBehavior extends ScrollBehavior {
......
...@@ -1593,6 +1593,177 @@ void main() { ...@@ -1593,6 +1593,177 @@ void main() {
expect(find.text('Bye, bye'), findsOneWidget); expect(find.text('Bye, bye'), findsOneWidget);
expect(tester.state(find.byType(Overlay)), same(overlayState)); expect(tester.state(find.byType(Overlay)), same(overlayState));
}); });
testWidgets('Overlay.wrap is sized by child in an unconstrained environment', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnconstrainedBox(
child: Overlay.wrap(
child: const Center(
child: SizedBox(
width: 123,
height: 456,
)
),
),
),
),
);
expect(tester.getSize(find.byType(Overlay)), const Size(123, 456));
});
testWidgets('Overlay is sized by child in an unconstrained environment', (WidgetTester tester) async {
final OverlayEntry initialEntry = OverlayEntry(
opaque: true,
canSizeOverlay: true,
builder: (BuildContext context) {
return const SizedBox(width: 123, height: 456);
}
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnconstrainedBox(
child: Overlay(
initialEntries: <OverlayEntry>[initialEntry]
),
),
),
);
expect(tester.getSize(find.byType(Overlay)), const Size(123, 456));
final OverlayState overlay = tester.state<OverlayState>(find.byType(Overlay));
final OverlayEntry nonSizingEntry = OverlayEntry(
builder: (BuildContext context) {
return const SizedBox(
width: 600,
height: 600,
child: Center(child: Text('Hello')),
);
},
);
overlay.insert(nonSizingEntry);
await tester.pump();
expect(tester.getSize(find.byType(Overlay)), const Size(123, 456));
expect(find.text('Hello'), findsOneWidget);
final OverlayEntry sizingEntry = OverlayEntry(
canSizeOverlay: true,
builder: (BuildContext context) {
return const SizedBox(
width: 222,
height: 111,
child: Center(child: Text('World')),
);
},
);
overlay.insert(sizingEntry);
await tester.pump();
expect(tester.getSize(find.byType(Overlay)), const Size(222, 111));
expect(find.text('Hello'), findsOneWidget);
expect(find.text('World'), findsOneWidget);
nonSizingEntry.remove();
await tester.pump();
expect(tester.getSize(find.byType(Overlay)), const Size(222, 111));
expect(find.text('Hello'), findsNothing);
expect(find.text('World'), findsOneWidget);
sizingEntry.remove();
await tester.pump();
expect(tester.getSize(find.byType(Overlay)), const Size(123, 456));
expect(find.text('Hello'), findsNothing);
expect(find.text('World'), findsNothing);
});
testWidgets('Overlay throws if unconstrained and has no child', (WidgetTester tester) async {
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
FlutterError.onError = errors.add;
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: UnconstrainedBox(
child: Overlay(),
),
),
);
FlutterError.onError = oldHandler;
expect(
errors.first.toString().replaceAll('\n', ' '),
contains('Overlay was given infinite constraints and cannot be sized by a suitable child.'),
);
});
testWidgets('Overlay throws if unconstrained and only positioned child', (WidgetTester tester) async {
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
FlutterError.onError = errors.add;
final OverlayEntry entry = OverlayEntry(
canSizeOverlay: true,
builder: (BuildContext context) {
return const Positioned(
top: 100,
child: SizedBox(width: 600, height: 600),
);
},
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnconstrainedBox(
child: Overlay(
initialEntries: <OverlayEntry>[entry],
),
),
),
);
FlutterError.onError = oldHandler;
expect(
errors.first.toString().replaceAll('\n', ' '),
contains('Overlay was given infinite constraints and cannot be sized by a suitable child.'),
);
});
testWidgets('Overlay throws if unconstrained and no canSizeOverlay child', (WidgetTester tester) async {
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
FlutterError.onError = errors.add;
final OverlayEntry entry = OverlayEntry(
builder: (BuildContext context) {
return const SizedBox(width: 600, height: 600);
},
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnconstrainedBox(
child: Overlay(
initialEntries: <OverlayEntry>[entry],
),
),
),
);
FlutterError.onError = oldHandler;
expect(
errors.first.toString().replaceAll('\n', ' '),
contains('Overlay was given infinite constraints and cannot be sized by a suitable child.'),
);
});
} }
class StatefulTestWidget extends StatefulWidget { class StatefulTestWidget extends StatefulWidget {
......
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