Unverified Commit e7ab3b07 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

`OverlayPortal` (#105335)

`OverlayPortal` 
parent 60de2aa9
// 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.
// Flutter code sample for OverlayPortal
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Code Sample',
home: Scaffold(
appBar: AppBar(title: const Text('OverlayPortal Example')),
body: const Center(child: ClickableTooltipWidget()),
),
);
}
}
class ClickableTooltipWidget extends StatefulWidget {
const ClickableTooltipWidget({super.key});
@override
State<StatefulWidget> createState() => ClickableTooltipWidgetState();
}
class ClickableTooltipWidgetState extends State<ClickableTooltipWidget> {
final OverlayPortalController _tooltipController = OverlayPortalController();
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: _tooltipController.toggle,
child: DefaultTextStyle(
style: DefaultTextStyle.of(context).style.copyWith(fontSize: 50),
child: OverlayPortal(
controller: _tooltipController,
overlayChildBuilder: (BuildContext context) {
return const Positioned(
right: 50,
bottom: 50,
child: ColoredBox(
color: Colors.amberAccent,
child: Text('tooltip'),
),
);
},
child: const Text('Press to show/hide tooltip'),
),
),
);
}
}
// 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 'package:flutter/rendering.dart';
import 'package:flutter_api_samples/widgets/overlay/overlay_portal.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
const String tooltipText = 'tooltip';
testWidgets('Tooltip is shown on press', (WidgetTester tester) async {
await tester.pumpWidget(const example.MyApp());
expect(find.text(tooltipText), findsNothing);
await tester.tap(find.byType(example.ClickableTooltipWidget));
await tester.pump();
expect(find.text(tooltipText), findsOneWidget);
await tester.tap(find.byType(example.ClickableTooltipWidget));
await tester.pump();
expect(find.text(tooltipText), findsNothing);
});
testWidgets('Tooltip is shown at the right location', (WidgetTester tester) async {
await tester.pumpWidget(const example.MyApp());
await tester.tap(find.byType(example.ClickableTooltipWidget));
await tester.pump();
final Size canvasSize = tester.getSize(find.byType(example.MyApp));
expect(
tester.getBottomRight(find.text(tooltipText)),
canvasSize - const Size(50, 50),
);
});
testWidgets('Tooltip is shown with the right font size', (WidgetTester tester) async {
await tester.pumpWidget(const example.MyApp());
await tester.tap(find.byType(example.ClickableTooltipWidget));
await tester.pump();
final TextSpan textSpan = tester.renderObject<RenderParagraph>(find.text(tooltipText)).text as TextSpan;
expect(textSpan.style?.fontSize, 50);
});
}
...@@ -641,7 +641,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController ...@@ -641,7 +641,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
} }
void _didChangeLayout() { void _didChangeLayout() {
if (_inkFeatures != null && _inkFeatures!.isNotEmpty) { if (_inkFeatures?.isNotEmpty ?? false) {
markNeedsPaint(); markNeedsPaint();
} }
} }
...@@ -651,16 +651,18 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController ...@@ -651,16 +651,18 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (_inkFeatures != null && _inkFeatures!.isNotEmpty) { final List<InkFeature>? inkFeatures = _inkFeatures;
if (inkFeatures != null && inkFeatures.isNotEmpty) {
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
canvas.save(); canvas.save();
canvas.translate(offset.dx, offset.dy); canvas.translate(offset.dx, offset.dy);
canvas.clipRect(Offset.zero & size); canvas.clipRect(Offset.zero & size);
for (final InkFeature inkFeature in _inkFeatures!) { for (final InkFeature inkFeature in inkFeatures) {
inkFeature._paint(canvas); inkFeature._paint(canvas);
} }
canvas.restore(); canvas.restore();
} }
assert(inkFeatures == _inkFeatures);
super.paint(context, offset); super.paint(context, offset);
} }
} }
...@@ -740,32 +742,71 @@ abstract class InkFeature { ...@@ -740,32 +742,71 @@ abstract class InkFeature {
onRemoved?.call(); onRemoved?.call();
} }
// Returns the paint transform that allows `fromRenderObject` to perform paint
// in `toRenderObject`'s coordinate space.
//
// Returns null if either `fromRenderObject` or `toRenderObject` is not in the
// same render tree, or either of them is in an offscreen subtree (see
// RenderObject.paintsChild).
static Matrix4? _getPaintTransform(
RenderObject fromRenderObject,
RenderObject toRenderObject,
) {
// The paths to fromRenderObject and toRenderObject's common ancestor.
final List<RenderObject> fromPath = <RenderObject>[fromRenderObject];
final List<RenderObject> toPath = <RenderObject>[toRenderObject];
RenderObject from = fromRenderObject;
RenderObject to = toRenderObject;
while (!identical(from, to)) {
final int fromDepth = from.depth;
final int toDepth = to.depth;
if (fromDepth >= toDepth) {
final AbstractNode? fromParent = from.parent;
// Return early if the 2 render objects are not in the same render tree,
// or either of them is offscreen and thus won't get painted.
if (fromParent is! RenderObject || !fromParent.paintsChild(from)) {
return null;
}
fromPath.add(fromParent);
from = fromParent;
}
if (fromDepth <= toDepth) {
final AbstractNode? toParent = to.parent;
if (toParent is! RenderObject || !toParent.paintsChild(to)) {
return null;
}
toPath.add(toParent);
to = toParent;
}
}
assert(identical(from, to));
final Matrix4 transform = Matrix4.identity();
final Matrix4 inverseTransform = Matrix4.identity();
for (int index = toPath.length - 1; index > 0; index -= 1) {
toPath[index].applyPaintTransform(toPath[index - 1], transform);
}
for (int index = fromPath.length - 1; index > 0; index -= 1) {
fromPath[index].applyPaintTransform(fromPath[index - 1], inverseTransform);
}
final double det = inverseTransform.invert();
return det != 0 ? (inverseTransform..multiply(transform)) : null;
}
void _paint(Canvas canvas) { void _paint(Canvas canvas) {
assert(referenceBox.attached); assert(referenceBox.attached);
assert(!_debugDisposed); assert(!_debugDisposed);
// find the chain of renderers from us to the feature's referenceBox
final List<RenderObject> descendants = <RenderObject>[referenceBox];
RenderObject node = referenceBox;
while (node != _controller) {
final RenderObject childNode = node;
node = node.parent! as RenderObject;
if (!node.paintsChild(childNode)) {
// Some node between the reference box and this would skip painting on
// the reference box, so bail out early and avoid unnecessary painting.
// Some cases where this can happen are the reference box being
// offstage, in a fully transparent opacity node, or in a keep alive
// bucket.
return;
}
descendants.add(node);
}
// determine the transform that gets our coordinate system to be like theirs // determine the transform that gets our coordinate system to be like theirs
final Matrix4 transform = Matrix4.identity(); final Matrix4? transform = _getPaintTransform(_controller, referenceBox);
assert(descendants.length >= 2); if (transform != null) {
for (int index = descendants.length - 1; index > 0; index -= 1) { paintFeature(canvas, transform);
descendants[index].applyPaintTransform(descendants[index - 1], transform);
} }
paintFeature(canvas, transform);
} }
/// Override this method to paint the ink feature. /// Override this method to paint the ink feature.
......
...@@ -1486,7 +1486,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1486,7 +1486,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// in other cases will lead to an inconsistent tree and probably cause crashes. /// in other cases will lead to an inconsistent tree and probably cause crashes.
@override @override
void adoptChild(RenderObject child) { void adoptChild(RenderObject child) {
assert(_debugCanPerformMutations);
setupParentData(child); setupParentData(child);
markNeedsLayout(); markNeedsLayout();
markNeedsCompositingBitsUpdate(); markNeedsCompositingBitsUpdate();
...@@ -1500,7 +1499,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1500,7 +1499,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// in other cases will lead to an inconsistent tree and probably cause crashes. /// in other cases will lead to an inconsistent tree and probably cause crashes.
@override @override
void dropChild(RenderObject child) { void dropChild(RenderObject child) {
assert(_debugCanPerformMutations);
assert(child.parentData != null); assert(child.parentData != null);
child._cleanRelayoutBoundary(); child._cleanRelayoutBoundary();
child.parentData!.detach(); child.parentData!.detach();
...@@ -1643,7 +1641,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1643,7 +1641,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
} }
if (!activeLayoutRoot._debugMutationsLocked) { if (!activeLayoutRoot._debugMutationsLocked) {
final AbstractNode? p = activeLayoutRoot.parent; final AbstractNode? p = activeLayoutRoot.debugLayoutParent;
activeLayoutRoot = p is RenderObject ? p : null; activeLayoutRoot = p is RenderObject ? p : null;
} else { } else {
// activeLayoutRoot found. // activeLayoutRoot found.
...@@ -1722,6 +1720,29 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1722,6 +1720,29 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
return result; return result;
} }
/// The [RenderObject] that's expected to call [layout] on this [RenderObject]
/// in its [performLayout] implementation.
///
/// This method is used to implement an assert that ensures the render subtree
/// actively performing layout can not get accidently mutated. It's only
/// implemented in debug mode and always returns null in release mode.
///
/// The default implementation returns [parent] and overriding is rarely
/// needed. A [RenderObject] subclass that expects its
/// [RenderObject.performLayout] to be called from a different [RenderObject]
/// that's not its [parent] should override this property to return the actual
/// layout parent.
@protected
RenderObject? get debugLayoutParent {
RenderObject? layoutParent;
assert(() {
final AbstractNode? parent = this.parent;
layoutParent = parent is RenderObject? ? parent : null;
return true;
}());
return layoutParent;
}
@override @override
PipelineOwner? get owner => super.owner as PipelineOwner?; PipelineOwner? get owner => super.owner as PipelineOwner?;
...@@ -3636,17 +3657,13 @@ mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject ...@@ -3636,17 +3657,13 @@ mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
super.attach(owner); super.attach(owner);
if (_child != null) { _child?.attach(owner);
_child!.attach(owner);
}
} }
@override @override
void detach() { void detach() {
super.detach(); super.detach();
if (_child != null) { _child?.detach();
_child!.detach();
}
} }
@override @override
......
...@@ -4701,6 +4701,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -4701,6 +4701,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
performRebuild(); performRebuild();
} finally { } finally {
assert(() { assert(() {
owner!._debugElementWasRebuilt(this);
assert(owner!._debugCurrentBuildTarget == this); assert(owner!._debugCurrentBuildTarget == this);
owner!._debugCurrentBuildTarget = debugPreviousBuildTarget; owner!._debugCurrentBuildTarget = debugPreviousBuildTarget;
return true; return true;
......
...@@ -17,6 +17,8 @@ import 'ticker_provider.dart'; ...@@ -17,6 +17,8 @@ import 'ticker_provider.dart';
// Examples can assume: // Examples can assume:
// late BuildContext context; // late BuildContext context;
// * OverlayEntry Implementation
/// A place in an [Overlay] that can contain a widget. /// A place in an [Overlay] that can contain a widget.
/// ///
/// Overlay entries are inserted into an [Overlay] using the /// Overlay entries are inserted into an [Overlay] using the
...@@ -127,21 +129,20 @@ class OverlayEntry implements Listenable { ...@@ -127,21 +129,20 @@ class OverlayEntry implements Listenable {
/// 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.
bool get mounted => _overlayStateMounted.value; bool get mounted => _overlayEntryStateNotifier.value != null;
/// Whether the `_OverlayState`s built using this [OverlayEntry] is currently /// The currently mounted `_OverlayEntryWidgetState` built using this [OverlayEntry].
/// mounted. final ValueNotifier<_OverlayEntryWidgetState?> _overlayEntryStateNotifier = ValueNotifier<_OverlayEntryWidgetState?>(null);
final ValueNotifier<bool> _overlayStateMounted = ValueNotifier<bool>(false);
@override @override
void addListener(VoidCallback listener) { void addListener(VoidCallback listener) {
assert(!_disposedByOwner); assert(!_disposedByOwner);
_overlayStateMounted.addListener(listener); _overlayEntryStateNotifier.addListener(listener);
} }
@override @override
void removeListener(VoidCallback listener) { void removeListener(VoidCallback listener) {
_overlayStateMounted.removeListener(listener); _overlayEntryStateNotifier.removeListener(listener);
} }
OverlayState? _overlay; OverlayState? _overlay;
...@@ -154,9 +155,9 @@ class OverlayEntry implements Listenable { ...@@ -154,9 +155,9 @@ class OverlayEntry implements Listenable {
/// This method removes this overlay entry from the overlay immediately. The /// This method removes this overlay entry from the overlay immediately. The
/// UI will be updated in the same frame if this method is called before the /// UI will be updated in the same frame if this method is called before the
/// overlay rebuild in this frame; otherwise, the UI will be updated in the /// overlay rebuild in this frame; otherwise, the UI will be updated in the
/// next frame. This means that it is safe to call during builds, but also /// next frame. This means that it is safe to call during builds, but also
/// that if you do call this after the overlay rebuild, the UI will not update /// that if you do call this after the overlay rebuild, the UI will not update
/// until the next frame (i.e. many milliseconds later). /// until the next frame (i.e. many milliseconds later).
void remove() { void remove() {
assert(_overlay != null); assert(_overlay != null);
assert(!_disposedByOwner); assert(!_disposedByOwner);
...@@ -187,7 +188,7 @@ class OverlayEntry implements Listenable { ...@@ -187,7 +188,7 @@ class OverlayEntry implements Listenable {
void _didUnmount() { void _didUnmount() {
assert(!mounted); assert(!mounted);
if (_disposedByOwner) { if (_disposedByOwner) {
_overlayStateMounted.dispose(); _overlayEntryStateNotifier.dispose();
} }
} }
...@@ -210,7 +211,7 @@ class OverlayEntry implements Listenable { ...@@ -210,7 +211,7 @@ class OverlayEntry implements Listenable {
assert(_overlay == null, 'An OverlayEntry must first be removed from the Overlay before dispose is called.'); assert(_overlay == null, 'An OverlayEntry must first be removed from the Overlay before dispose is called.');
_disposedByOwner = true; _disposedByOwner = true;
if (!mounted) { if (!mounted) {
_overlayStateMounted.dispose(); _overlayEntryStateNotifier.dispose();
} }
} }
...@@ -222,10 +223,12 @@ class _OverlayEntryWidget extends StatefulWidget { ...@@ -222,10 +223,12 @@ class _OverlayEntryWidget extends StatefulWidget {
const _OverlayEntryWidget({ const _OverlayEntryWidget({
required Key key, required Key key,
required this.entry, required this.entry,
required this.overlayState,
this.tickerEnabled = true, this.tickerEnabled = true,
}) : super(key: key); }) : super(key: key);
final OverlayEntry entry; final OverlayEntry entry;
final OverlayState overlayState;
final bool tickerEnabled; final bool tickerEnabled;
@override @override
...@@ -233,16 +236,102 @@ class _OverlayEntryWidget extends StatefulWidget { ...@@ -233,16 +236,102 @@ class _OverlayEntryWidget extends StatefulWidget {
} }
class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> { class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
late _RenderTheater _theater;
// Manages the stack of theater children whose paint order are sorted by their
// _zOrderIndex. The children added by OverlayPortal are added to this linked
// list, and they will be shown _above_ the OverlayEntry tied to this widget.
// The children with larger zOrderIndex values (i.e. those called `show`
// recently) will be painted last.
//
// This linked list is lazily created in `_add`, and the entries are added/removed
// via `_add`/`_remove`, called by OverlayPortals lower in the tree. `_add` or
// `_remove` does not cause this widget to rebuild, the linked list will be
// read by _RenderTheater as part of its render child model. This would ideally
// be in a RenderObject but there may not be RenderObjects between
// _RenderTheater and the render subtree OverlayEntry builds.
LinkedList<_OverlayEntryLocation>? _sortedTheaterSiblings;
// Worst-case O(N), N being the number of children added to the top spot in
// the same frame. This can be a bit expensive when there's a lot of global
// key reparenting in the same frame but N is usually a small number.
void _add(_OverlayEntryLocation child) {
assert(mounted);
final LinkedList<_OverlayEntryLocation> children = _sortedTheaterSiblings ??= LinkedList<_OverlayEntryLocation>();
assert(!children.contains(child));
_OverlayEntryLocation? insertPosition = children.isEmpty ? null : children.last;
while (insertPosition != null && insertPosition._zOrderIndex > child._zOrderIndex) {
insertPosition = insertPosition.previous;
}
if (insertPosition == null) {
children.addFirst(child);
} else {
insertPosition.insertAfter(child);
}
assert(children.contains(child));
}
void _remove(_OverlayEntryLocation child) {
assert(_sortedTheaterSiblings != null);
final bool wasInCollection = _sortedTheaterSiblings?.remove(child) ?? false;
assert(wasInCollection);
}
// Returns an Iterable that traverse the children in the child model in paint
// order (from farthest to the user to the closest to the user).
//
// The iterator should be safe to use even when the child model is being
// mutated. The reason for that is it's allowed to add/remove/move deferred
// children to a _RenderTheater during performLayout, but the affected
// children don't have to be laid out in the same performLayout call.
late final Iterable<RenderBox> _paintOrderIterable = _createChildIterable(reversed: false);
// An Iterable that traverse the children in the child model in
// hit-test order (from closest to the user to the farthest to the user).
late final Iterable<RenderBox> _hitTestOrderIterable = _createChildIterable(reversed: true);
// The following uses sync* because hit-testing is lazy, and LinkedList as a
// Iterable doesn't support current modification.
Iterable<RenderBox> _createChildIterable({ required bool reversed }) sync* {
final LinkedList<_OverlayEntryLocation>? children = _sortedTheaterSiblings;
if (children == null || children.isEmpty) {
return;
}
_OverlayEntryLocation? candidate = reversed ? children.last : children.first;
while(candidate != null) {
final RenderBox? renderBox = candidate._overlayChildRenderBox;
candidate = reversed ? candidate.previous : candidate.next;
if (renderBox != null) {
yield renderBox;
}
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
widget.entry._overlayStateMounted.value = true; widget.entry._overlayEntryStateNotifier.value = this;
_theater = context.findAncestorRenderObjectOfType<_RenderTheater>()!;
assert(_sortedTheaterSiblings == null);
}
@override
void didUpdateWidget(_OverlayEntryWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// OverlayState's build method always returns a RenderObjectWidget _Theater,
// so it's safe to assume that state equality implies render object equality.
assert(oldWidget.entry == widget.entry);
if (oldWidget.overlayState != widget.overlayState) {
final _RenderTheater newTheater = context.findAncestorRenderObjectOfType<_RenderTheater>()!;
assert(_theater != newTheater);
_theater = newTheater;
}
} }
@override @override
void dispose() { void dispose() {
widget.entry._overlayStateMounted.value = false; widget.entry._overlayEntryStateNotifier.value = null;
widget.entry._didUnmount(); widget.entry._didUnmount();
_sortedTheaterSiblings = null;
super.dispose(); super.dispose();
} }
...@@ -250,7 +339,11 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> { ...@@ -250,7 +339,11 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TickerMode( return TickerMode(
enabled: widget.tickerEnabled, enabled: widget.tickerEnabled,
child: widget.entry.builder(context), child: _RenderTheaterMarker(
theater: _theater,
overlayEntryWidgetState: this,
child: widget.entry.builder(context),
),
); );
} }
...@@ -594,15 +687,15 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin { ...@@ -594,15 +687,15 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// This list is filled backwards and then reversed below before // This list is filled backwards and then reversed below before
// it is added to the tree. // it is added to the tree.
final List<Widget> children = <Widget>[]; final List<_OverlayEntryWidget> children = <_OverlayEntryWidget>[];
bool onstage = true; bool onstage = true;
int onstageCount = 0; int onstageCount = 0;
for (int i = _entries.length - 1; i >= 0; i -= 1) { for (final OverlayEntry entry in _entries.reversed) {
final OverlayEntry entry = _entries[i];
if (onstage) { if (onstage) {
onstageCount += 1; onstageCount += 1;
children.add(_OverlayEntryWidget( children.add(_OverlayEntryWidget(
key: entry._key, key: entry._key,
overlayState: this,
entry: entry, entry: entry,
)); ));
if (entry.opaque) { if (entry.opaque) {
...@@ -611,12 +704,13 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin { ...@@ -611,12 +704,13 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin {
} else if (entry.maintainState) { } else if (entry.maintainState) {
children.add(_OverlayEntryWidget( children.add(_OverlayEntryWidget(
key: entry._key, key: entry._key,
overlayState: this,
entry: entry, entry: entry,
tickerEnabled: false, tickerEnabled: false,
)); ));
} }
} }
return _Theatre( return _Theater(
skipCount: children.length - onstageCount, skipCount: children.length - onstageCount,
clipBehavior: widget.clipBehavior, clipBehavior: widget.clipBehavior,
children: children.reversed.toList(growable: false), children: children.reversed.toList(growable: false),
...@@ -636,11 +730,11 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin { ...@@ -636,11 +730,11 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin {
/// [skipCount] children. /// [skipCount] children.
/// ///
/// The first [skipCount] children are considered "offstage". /// The first [skipCount] children are considered "offstage".
class _Theatre extends MultiChildRenderObjectWidget { class _Theater extends MultiChildRenderObjectWidget {
const _Theatre({ const _Theater({
this.skipCount = 0, this.skipCount = 0,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
super.children, required List<_OverlayEntryWidget> super.children,
}) : assert(skipCount >= 0), }) : assert(skipCount >= 0),
assert(children.length >= skipCount); assert(children.length >= skipCount);
...@@ -649,11 +743,11 @@ class _Theatre extends MultiChildRenderObjectWidget { ...@@ -649,11 +743,11 @@ class _Theatre extends MultiChildRenderObjectWidget {
final Clip clipBehavior; final Clip clipBehavior;
@override @override
_TheatreElement createElement() => _TheatreElement(this); _TheaterElement createElement() => _TheaterElement(this);
@override @override
_RenderTheatre createRenderObject(BuildContext context) { _RenderTheater createRenderObject(BuildContext context) {
return _RenderTheatre( return _RenderTheater(
skipCount: skipCount, skipCount: skipCount,
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
...@@ -661,7 +755,7 @@ class _Theatre extends MultiChildRenderObjectWidget { ...@@ -661,7 +755,7 @@ class _Theatre extends MultiChildRenderObjectWidget {
} }
@override @override
void updateRenderObject(BuildContext context, _RenderTheatre renderObject) { void updateRenderObject(BuildContext context, _RenderTheater renderObject) {
renderObject renderObject
..skipCount = skipCount ..skipCount = skipCount
..textDirection = Directionality.of(context) ..textDirection = Directionality.of(context)
...@@ -675,22 +769,114 @@ class _Theatre extends MultiChildRenderObjectWidget { ...@@ -675,22 +769,114 @@ class _Theatre extends MultiChildRenderObjectWidget {
} }
} }
class _TheatreElement extends MultiChildRenderObjectElement { class _TheaterElement extends MultiChildRenderObjectElement {
_TheatreElement(_Theatre super.widget); _TheaterElement(_Theater super.widget);
@override @override
_RenderTheatre get renderObject => super.renderObject as _RenderTheatre; _RenderTheater get renderObject => super.renderObject as _RenderTheater;
@override
void insertRenderObjectChild(RenderBox child, IndexedSlot<Element?> slot) {
super.insertRenderObjectChild(child, slot);
final _TheaterParentData parentData = child.parentData! as _TheaterParentData;
parentData.overlayEntry = ((widget as _Theater).children[slot.index] as _OverlayEntryWidget).entry;
assert(parentData.overlayEntry != null);
}
@override
void moveRenderObjectChild(RenderBox child, IndexedSlot<Element?> oldSlot, IndexedSlot<Element?> newSlot) {
super.moveRenderObjectChild(child, oldSlot, newSlot);
assert(() {
final _TheaterParentData parentData = child.parentData! as _TheaterParentData;
return parentData.overlayEntry == ((widget as _Theater).children[newSlot.index] as _OverlayEntryWidget).entry;
}());
}
@override @override
void debugVisitOnstageChildren(ElementVisitor visitor) { void debugVisitOnstageChildren(ElementVisitor visitor) {
final _Theatre theatre = widget as _Theatre; final _Theater theater = widget as _Theater;
assert(children.length >= theatre.skipCount); assert(children.length >= theater.skipCount);
children.skip(theatre.skipCount).forEach(visitor); children.skip(theater.skipCount).forEach(visitor);
} }
} }
class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox, StackParentData> { // A `RenderBox` that sizes itself to its parent's size, implements the stack
_RenderTheatre({ // layout algorithm and renders its children in the given `theater`.
mixin _RenderTheaterMixin on RenderBox {
_RenderTheater get theater;
Iterable<RenderBox> _childrenInPaintOrder();
Iterable<RenderBox> _childrenInHitTestOrder();
@override
void setupParentData(RenderBox child) {
if (child.parentData is! StackParentData) {
child.parentData = StackParentData();
}
}
@override
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;
if (!childParentData.isPositioned) {
child.layout(nonPositionedChildConstraints, parentUsesSize: true);
childParentData.offset = alignment.alongOffset(size - child.size as Offset);
} else {
assert(child is! _RenderDeferredLayoutBox, 'all _RenderDeferredLayoutBoxes must be non-positioned children.');
RenderStack.layoutPositionedChild(child, childParentData, size, alignment);
}
assert(child.parentData == childParentData);
}
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
final Iterator<RenderBox> iterator = _childrenInHitTestOrder().iterator;
bool isHit = false;
while (!isHit && iterator.moveNext()) {
final RenderBox child = iterator.current;
final StackParentData childParentData = child.parentData! as StackParentData;
final RenderBox localChild = child;
bool childHitTest(BoxHitTestResult result, Offset position) => localChild.hitTest(result, position: position);
isHit = result.addWithPaintOffset(offset: childParentData.offset, position: position, hitTest: childHitTest);
}
return isHit;
}
@override
void paint(PaintingContext context, Offset offset) {
for (final RenderBox child in _childrenInPaintOrder()) {
final StackParentData childParentData = child.parentData! as StackParentData;
context.paintChild(child, childParentData.offset + offset);
}
}
}
class _TheaterParentData extends StackParentData {
// The OverlayEntry that directly created this child. This field is null for
// children that are created by an OverlayPortal.
OverlayEntry? overlayEntry;
// _overlayStateMounted is set to null in _OverlayEntryWidgetState's dispose
// method. This property is only accessed during layout, paint and hit-test so
// the `value!` should be safe.
Iterator<RenderBox>? get paintOrderIterator => overlayEntry?._overlayEntryStateNotifier.value!._paintOrderIterable.iterator;
Iterator<RenderBox>? get hitTestOrderIterator => overlayEntry?._overlayEntryStateNotifier.value!._hitTestOrderIterable.iterator;
void visitChildrenOfOverlayEntry(RenderObjectVisitor visitor) => overlayEntry?._overlayEntryStateNotifier.value!._paintOrderIterable.forEach(visitor);
}
class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox, StackParentData>, _RenderTheaterMixin {
_RenderTheater({
List<RenderBox>? children, List<RenderBox>? children,
required TextDirection textDirection, required TextDirection textDirection,
int skipCount = 0, int skipCount = 0,
...@@ -702,24 +888,53 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox ...@@ -702,24 +888,53 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox
addAll(children); addAll(children);
} }
@override
_RenderTheater get theater => this;
@override @override
void setupParentData(RenderBox child) { void setupParentData(RenderBox child) {
if (child.parentData is! StackParentData) { if (child.parentData is! _TheaterParentData) {
child.parentData = StackParentData(); child.parentData = _TheaterParentData();
} }
} }
Alignment? _resolvedAlignment; @override
void attach(PipelineOwner owner) {
super.attach(owner);
RenderBox? child = firstChild;
while (child != null) {
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
final Iterator<RenderBox>? iterator = childParentData.paintOrderIterator;
if (iterator != null) {
while(iterator.moveNext()) {
iterator.current.attach(owner);
}
}
child = childParentData.nextSibling;
}
}
void _resolve() { static void _detachChild(RenderObject child) => child.detach();
if (_resolvedAlignment != null) {
return; @override
void detach() {
super.detach();
RenderBox? child = firstChild;
while (child != null) {
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
childParentData.visitChildrenOfOverlayEntry(_detachChild);
child = childParentData.nextSibling;
} }
_resolvedAlignment = AlignmentDirectional.topStart.resolve(textDirection);
} }
@override
void redepthChildren() => visitChildren(redepthChild);
Alignment? _alignmentCache;
Alignment get _resolvedAlignment => _alignmentCache ??= AlignmentDirectional.topStart.resolve(textDirection);
void _markNeedResolution() { void _markNeedResolution() {
_resolvedAlignment = null; _alignmentCache = null;
markNeedsLayout(); markNeedsLayout();
} }
...@@ -755,6 +970,38 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox ...@@ -755,6 +970,38 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox
} }
} }
// Adding/removing deferred child does not affect the layout of other children,
// or that of the Overlay, so there's no need to invalidate the layout of the
// Overlay.
//
// When _skipMarkNeedsLayout is true, markNeedsLayout does not do anything.
bool _skipMarkNeedsLayout = false;
void _addDeferredChild(_RenderDeferredLayoutBox child) {
assert(!_skipMarkNeedsLayout);
_skipMarkNeedsLayout = true;
adoptChild(child);
// When child has never been laid out before, mark its layout surrogate as
// needing layout so it's reachable via tree walk.
child._layoutSurrogate.markNeedsLayout();
_skipMarkNeedsLayout = false;
}
void _removeDeferredChild(_RenderDeferredLayoutBox child) {
assert(!_skipMarkNeedsLayout);
_skipMarkNeedsLayout = true;
dropChild(child);
_skipMarkNeedsLayout = false;
}
@override
void markNeedsLayout() {
if (_skipMarkNeedsLayout) {
return;
}
super.markNeedsLayout();
}
RenderBox? get _firstOnstageChild { RenderBox? get _firstOnstageChild {
if (skipCount == super.childCount) { if (skipCount == super.childCount) {
return null; return null;
...@@ -770,8 +1017,6 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox ...@@ -770,8 +1017,6 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox
RenderBox? get _lastOnstageChild => skipCount == super.childCount ? null : lastChild; RenderBox? get _lastOnstageChild => skipCount == super.childCount ? null : lastChild;
int get _onstageChildCount => childCount - skipCount;
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicWidth(height)); return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicWidth(height));
...@@ -814,9 +1059,6 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox ...@@ -814,9 +1059,6 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox
return result; return result;
} }
@override
bool get sizedByParent => true;
@override @override
Size computeDryLayout(BoxConstraints constraints) { Size computeDryLayout(BoxConstraints constraints) {
assert(constraints.biggest.isFinite); assert(constraints.biggest.isFinite);
...@@ -824,64 +1066,43 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox ...@@ -824,64 +1066,43 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox
} }
@override @override
void performLayout() { // The following uses sync* because concurrent modifications should be allowed
if (_onstageChildCount == 0) { // during layout.
return; Iterable<RenderBox> _childrenInPaintOrder() sync* {
}
_resolve();
assert(_resolvedAlignment != null);
// Same BoxConstraints as used by RenderStack for StackFit.expand.
final BoxConstraints nonPositionedConstraints = BoxConstraints.tight(constraints.biggest);
RenderBox? child = _firstOnstageChild; RenderBox? child = _firstOnstageChild;
while (child != null) { while (child != null) {
final StackParentData childParentData = child.parentData! as StackParentData; yield child;
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
if (!childParentData.isPositioned) { final Iterator<RenderBox>? innerIterator = childParentData.paintOrderIterator;
child.layout(nonPositionedConstraints, parentUsesSize: true); if (innerIterator != null) {
childParentData.offset = _resolvedAlignment!.alongOffset(size - child.size as Offset); while (innerIterator.moveNext()) {
} else { yield innerIterator.current;
RenderStack.layoutPositionedChild(child, childParentData, size, _resolvedAlignment!); }
} }
assert(child.parentData == childParentData);
child = childParentData.nextSibling; child = childParentData.nextSibling;
} }
} }
@override @override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { // The following uses sync* because hit testing should be lazy.
Iterable<RenderBox> _childrenInHitTestOrder() sync* {
RenderBox? child = _lastOnstageChild; RenderBox? child = _lastOnstageChild;
for (int i = 0; i < _onstageChildCount; i++) { int childLeft = childCount - skipCount;
assert(child != null); while (child != null) {
final StackParentData childParentData = child!.parentData! as StackParentData; final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
final bool isHit = result.addWithPaintOffset( final Iterator<RenderBox>? innerIterator = childParentData.hitTestOrderIterator;
offset: childParentData.offset, if (innerIterator != null) {
position: position, while (innerIterator.moveNext()) {
hitTest: (BoxHitTestResult result, Offset transformed) { yield innerIterator.current;
assert(transformed == position - childParentData.offset); }
return child!.hitTest(result, position: transformed);
},
);
if (isHit) {
return true;
} }
child = childParentData.previousSibling; yield child;
childLeft -= 1;
child = childLeft <= 0 ? null : childParentData.previousSibling;
} }
return false;
} }
@protected final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
void paintStack(PaintingContext context, Offset offset) {
RenderBox? child = _firstOnstageChild;
while (child != null) {
final StackParentData childParentData = child.parentData! as StackParentData;
context.paintChild(child, childParentData.offset + offset);
child = childParentData.nextSibling;
}
}
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
...@@ -890,30 +1111,40 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox ...@@ -890,30 +1111,40 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox
needsCompositing, needsCompositing,
offset, offset,
Offset.zero & size, Offset.zero & size,
paintStack, super.paint,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer: _clipRectLayer.layer, oldLayer: _clipRectLayer.layer,
); );
} else { } else {
_clipRectLayer.layer = null; _clipRectLayer.layer = null;
paintStack(context, offset); super.paint(context, offset);
} }
} }
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override @override
void dispose() { void dispose() {
_clipRectLayer.layer = null; _clipRectLayer.layer = null;
super.dispose(); super.dispose();
} }
@override
void visitChildren(RenderObjectVisitor visitor) {
RenderBox? child = firstChild;
while (child != null) {
visitor(child);
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
childParentData.visitChildrenOfOverlayEntry(visitor);
child = childParentData.nextSibling;
}
}
@override @override
void visitChildrenForSemantics(RenderObjectVisitor visitor) { void visitChildrenForSemantics(RenderObjectVisitor visitor) {
RenderBox? child = _firstOnstageChild; RenderBox? child = _firstOnstageChild;
while (child != null) { while (child != null) {
visitor(child); visitor(child);
final StackParentData childParentData = child.parentData! as StackParentData; final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
childParentData.visitChildrenOfOverlayEntry(visitor);
child = childParentData.nextSibling; child = childParentData.nextSibling;
} }
} }
...@@ -947,6 +1178,7 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox ...@@ -947,6 +1178,7 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox
RenderBox? child = firstChild; RenderBox? child = firstChild;
final RenderBox? firstOnstageChild = _firstOnstageChild; final RenderBox? firstOnstageChild = _firstOnstageChild;
while (child != null) { while (child != null) {
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
if (child == firstOnstageChild) { if (child == firstOnstageChild) {
onstage = true; onstage = true;
count = 1; count = 1;
...@@ -967,7 +1199,26 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox ...@@ -967,7 +1199,26 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox
); );
} }
final StackParentData childParentData = child.parentData! as StackParentData; int subcount = 1;
childParentData.visitChildrenOfOverlayEntry((RenderObject renderObject) {
final RenderBox child = renderObject as RenderBox;
if (onstage) {
onstageChildren.add(
child.toDiagnosticsNode(
name: 'onstage $count - $subcount',
),
);
} else {
offstageChildren.add(
child.toDiagnosticsNode(
name: 'offstage $count - $subcount',
style: DiagnosticsTreeStyle.offstage,
),
);
}
subcount += 1;
});
child = childParentData.nextSibling; child = childParentData.nextSibling;
count += 1; count += 1;
} }
...@@ -984,3 +1235,876 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox ...@@ -984,3 +1235,876 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox
]; ];
} }
} }
// * OverlayPortal Implementation
// OverlayPortal is inspired by the
// [flutter_portal](https://pub.dev/packages/flutter_portal) package.
//
// ** RenderObject hierarchy
// The widget works by inserting its overlay child's render subtree directly
// under [Overlay]'s render object (_RenderTheater).
// https://user-images.githubusercontent.com/31859944/171971838-62ed3975-4b5d-4733-a9c9-f79e263b8fcc.jpg
//
// To ensure the overlay child render subtree does not do layout twice, the
// subtree must only perform layout after both its _RenderTheater and the
// [OverlayPortal]'s render object (_RenderLayoutSurrogateProxyBox) have
// finished layout. This is handled by _RenderDeferredLayoutBox.
//
// ** Z-Index of an overlay child
// [_OverlayEntryLocation] is a (currently private) interface that allows an
// [OverlayPortal] to insert its overlay child into a specific [Overlay], as
// well as specifying the paint order between the overlay child and other
// children of the _RenderTheater.
//
// Since [OverlayPortal] is only allowed to target ancestor [Overlay]s
// (_RenderTheater must finish doing layout before _RenderDeferredLayoutBox),
// the _RenderTheater should typically be acquired using an [InheritedWidget]
// (currently, _RenderTheaterMarker) in case the [OverlayPortal] gets
// reparented.
/// A class to show, hide and bring to top an [OverlayPortal]'s overlay child
/// in the target [Overlay].
///
/// A [OverlayPortalController] can only be given to at most one [OverlayPortal]
/// at a time. When an [OverlayPortalController] is moved from one
/// [OverlayPortal] to another, its [isShowing] state does not carry over.
///
/// [OverlayPortalController.show] and [OverlayPortalController.hide] can be
/// called even before the controller is assigned to any [OverlayPortal], but
/// they typically should not be called while the widget tree is being rebuilt.
class OverlayPortalController {
/// Creates an [OverlayPortalController], optionally with a String identifier
/// `debugLabel`.
OverlayPortalController({ String? debugLabel }) : _debugLabel = debugLabel;
_OverlayPortalState? _attachTarget;
// A separate _zOrderIndex to allow `show()` or `hide()` to be called when the
// controller is not yet attached. Once this controller is attached,
// _attachTarget._zOrderIndex will be used as the source of truth, and this
// variable will be set to null.
int? _zOrderIndex;
final String? _debugLabel;
static int _wallTime = kIsWeb
? -9007199254740992 // -2^53
: -1 << 63;
// Returns a unique and monotonically increasing timestamp that represents
// now.
//
// The value this method returns increments after each call.
int _now() {
final int now = _wallTime += 1;
assert(_zOrderIndex == null || _zOrderIndex! < now);
assert(_attachTarget?._zOrderIndex == null || _attachTarget!._zOrderIndex! < now);
return now;
}
/// Show the overlay child of the [OverlayPortal] this controller is attached
/// to, at the top of the target [Overlay].
///
/// When there are more than one [OverlayPortal]s that target the same
/// [Overlay], the overlay child of the last [OverlayPortal] to have called
/// [show] appears at the top level, unobstructed.
///
/// If [isShowing] is already true, calling this method brings the overlay
/// child it controls to the top.
///
/// This method should typically not be called while the widget tree is being
/// rebuilt.
void show() {
final _OverlayPortalState? state = _attachTarget;
if (state != null) {
state.show(_now());
} else {
_zOrderIndex = _now();
}
}
/// Hide the [OverlayPortal]'s overlay child.
///
/// Once hidden, the overlay child will be removed from the widget tree the
/// next time the widget tree rebuilds, and stateful widgets in the overlay
/// child may lose states as a result.
///
/// This method should typically not be called while the widget tree is being
/// rebuilt.
void hide() {
final _OverlayPortalState? state = _attachTarget;
if (state != null) {
state.hide();
} else {
assert(_zOrderIndex != null);
_zOrderIndex = null;
}
}
/// Whether the associated [OverlayPortal] should build and show its overlay
/// child, using its `overlayChildBuilder`.
bool get isShowing {
final _OverlayPortalState? state = _attachTarget;
return state != null
? state._zOrderIndex != null
: _zOrderIndex != null;
}
/// Conventience method for toggling the current [isShowing] status.
///
/// This method should typically not be called while the widget tree is being
/// rebuilt.
void toggle() => isShowing ? hide() : show();
@override
String toString() {
final String? debugLabel = _debugLabel;
final String label = debugLabel == null ? '' : '($debugLabel)';
final String isDetached = _attachTarget != null ? '' : ' DETACHED';
return '${objectRuntimeType(this, 'OverlayPortalController')}$label$isDetached';
}
}
/// A widget that renders its overlay child on an [Overlay].
///
/// The overlay child is initially hidden until [OverlayPortalController.show]
/// is called on the associated [controller]. The [OverlayPortal] uses
/// [overlayChildBuilder] to build its overlay child and renders it on the
/// specified [Overlay] as if it was inserted using an [OverlayEntry], while it
/// can depend on the same set of [InheritedWidget]s (such as [Theme]) that this
/// widget can depend on.
///
/// This widget requires an [Overlay] ancestor in the widget tree when its
/// overlay child is showing.
///
/// When [OverlayPortalController.hide] is called, the widget built using
/// [overlayChildBuilder] will be removed from the widget tree the next time the
/// widget rebuilds. Stateful descendants in the overlay child subtree may lose
/// states as a result.
///
/// {@tool dartpad}
/// This example uses an [OverlayPortal] to build a tooltip that becomes visible
/// when the user taps on the [child] widget. There's a [DefaultTextStyle] above
/// the [OverlayPortal] controlling the [TextStyle] of both the [child] widget
/// and the widget [overlayChildBuilder] builds, which isn't otherwise doable if
/// the tooltip was added as an [OverlayEntry].
///
/// ** See code in examples/api/lib/widgets/overlay/overlay_portal.0.dart **
/// {@end-tool}
///
/// ### Paint Order
///
/// In an [Overlay], an overlay child is painted after the [OverlayEntry]
/// associated with its [OverlayPortal] (that is, the [OverlayEntry] closest to
/// the [OverlayPortal] in the widget tree, which usually represents the
/// enclosing [Route]), and before the next [OverlayEntry].
///
/// When an [OverlayEntry] has multiple associated [OverlayPortal]s, the paint
/// order between their overlay children is the order in which
/// [OverlayPortalController.show] was called. The last [OverlayPortal] to have
/// called `show` gets to paint its overlay child in the foreground.
///
/// ### Differences between [OverlayPortal] and [OverlayEntry]
///
/// The main difference between [OverlayEntry] and [OverlayPortal] is that
/// [OverlayEntry] builds its widget subtree as a child of the target [Overlay],
/// while [OverlayPortal] uses [overlayChildBuilder] to build a child widget of
/// itself. This allows [OverlayPortal]'s overlay child to depend on the same
/// set of [InheritedWidget]s as [OverlayPortal], and it's also guaranteed that
/// the overlay child will not outlive its [OverlayPortal].
///
/// On the other hand, [OverlayPortal]'s implementation is more complex. For
/// instance, it does a bit more work than a regular widget during global key
/// reparenting. If the content to be shown on the [Overlay] doesn't benefit
/// from being a part of [OverlayPortal]'s subtree, consider using an
/// [OverlayEntry] instead.
///
/// See also:
///
/// * [OverlayEntry], an alternative API for inserting widgets into an
/// [Overlay].
/// * [Positioned], which can be used to size and position the overlay child in
/// relation to the target [Overlay]'s boundaries.
/// * [CompositedTransformFollower], which can be used to position the overlay
/// child in relation to the linked [CompositedTransformTarget] widget.
class OverlayPortal extends StatefulWidget {
/// Creates an [OverlayPortal] that renders the widget [overlayChildBuilder]
/// builds on the closest [Overlay] when [OverlayPortalController.show] is
/// called.
const OverlayPortal({
super.key,
required this.controller,
required this.overlayChildBuilder,
this.child,
}) : _targetRootOverlay = false;
/// Creates an [OverlayPortal] that renders the widget [overlayChildBuilder]
/// builds on the root [Overlay] when [OverlayPortalController.show] is
/// called.
const OverlayPortal.targetsRootOverlay({
super.key,
required this.controller,
required this.overlayChildBuilder,
this.child,
}) : _targetRootOverlay = true;
/// The controller to show, hide and bring to top the overlay child.
final OverlayPortalController controller;
/// A [WidgetBuilder] used to build a widget below this widget in the tree,
/// that renders on the closest [Overlay].
///
/// The said widget will only be built and shown in the closest [Overlay] once
/// [OverlayPortalController.show] is called on the associated [controller].
/// It will be painted in front of the [OverlayEntry] closest to this widget
/// in the widget tree (which is usually the enclosing [Route]).
///
/// The built overlay child widget is inserted below this widget in the widget
/// tree, allowing it to depend on [InheritedWidget]s above it, and be
/// notified when the [InheritedWidget]s change.
///
/// Unlike [child], the built overlay child can visually extend outside the
/// bounds of this widget without being clipped, and receive hit-test events
/// outside of this widget's bounds, as long as it does not extend outside of
/// the [Overlay] on which it is rendered.
final WidgetBuilder overlayChildBuilder;
/// A widget below this widget in the tree.
final Widget? child;
final bool _targetRootOverlay;
@override
State<OverlayPortal> createState() => _OverlayPortalState();
}
class _OverlayPortalState extends State<OverlayPortal> {
int? _zOrderIndex;
// The location of the overlay child within the overlay. This object will be
// used as the slot of the overlay child widget.
//
// The developer must call `show` to reveal the overlay so we can get a unique
// timestamp of the user interaction for sorting.
//
// Avoid invalidating the cache if possible, since the framework uses `==` to
// compare slots, and _OverlayEntryLocation can't override that operator since
// it's mutable.
bool _childModelMayHaveChanged = true;
_OverlayEntryLocation? _locationCache;
_OverlayEntryLocation _getLocation(int zOrderIndex, bool targetRootOverlay) {
final _OverlayEntryLocation? cachedLocation = _locationCache;
if (cachedLocation != null && !_childModelMayHaveChanged) {
assert(cachedLocation._zOrderIndex == zOrderIndex);
return cachedLocation;
}
_childModelMayHaveChanged = false;
final _RenderTheaterMarker? marker = _RenderTheaterMarker.maybeOf(context, targetRootOverlay: targetRootOverlay);
if (marker == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('No Overlay widget found.'),
ErrorDescription(
'${widget.runtimeType} widgets require an Overlay widget ancestor.\n'
'An overlay lets widgets float on top of other widget children.',
),
ErrorHint(
'To introduce an Overlay widget, you can either directly '
'include one, or use a widget that contains an Overlay itself, '
'such as a Navigator, WidgetApp, MaterialApp, or CupertinoApp.',
),
...context.describeMissingAncestor(expectedAncestorType: Overlay),
]);
}
final _OverlayEntryLocation returnValue;
if (cachedLocation == null) {
returnValue = _OverlayEntryLocation(zOrderIndex, marker.overlayEntryWidgetState, marker.theater);
} else if (cachedLocation._childModel != marker.overlayEntryWidgetState || cachedLocation._theater != marker.theater) {
cachedLocation._dispose();
returnValue = _OverlayEntryLocation(zOrderIndex, marker.overlayEntryWidgetState, marker.theater);
} else {
returnValue = cachedLocation;
}
assert(returnValue._zOrderIndex == zOrderIndex);
return _locationCache = returnValue;
}
@override
void initState() {
super.initState();
_setupController(widget.controller);
}
void _setupController(OverlayPortalController controller) {
assert(
controller._attachTarget == null || controller._attachTarget == this,
'Failed to attach $controller to $this. It is already attached to ${controller._attachTarget}.'
);
final int? controllerZOrderIndex = controller._zOrderIndex;
final int? zOrderIndex = _zOrderIndex;
if (zOrderIndex == null || (controllerZOrderIndex != null && controllerZOrderIndex > zOrderIndex)) {
_zOrderIndex = controllerZOrderIndex;
}
controller._zOrderIndex = null;
controller._attachTarget = this;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_childModelMayHaveChanged = true;
}
@override
void didUpdateWidget(OverlayPortal oldWidget) {
super.didUpdateWidget(oldWidget);
_childModelMayHaveChanged = _childModelMayHaveChanged || oldWidget._targetRootOverlay != widget._targetRootOverlay;
if (oldWidget.controller != widget.controller) {
oldWidget.controller._attachTarget = null;
_setupController(widget.controller);
}
}
@override
void dispose() {
widget.controller._attachTarget = null;
_locationCache?._dispose();
_locationCache = null;
super.dispose();
}
void show(int zOrderIndex) {
assert(
SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks,
'${widget.controller.runtimeType}.show() should not be called during build.'
);
setState(() { _zOrderIndex = zOrderIndex; });
_locationCache?._dispose();
_locationCache = null;
}
void hide() {
assert(SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks);
setState(() { _zOrderIndex = null; });
_locationCache?._dispose();
_locationCache = null;
}
@override
Widget build(BuildContext context) {
final int? zOrderIndex = _zOrderIndex;
if (zOrderIndex == null) {
return _OverlayPortal(
overlayLocation: null,
overlayChild: null,
child: widget.child,
);
}
return _OverlayPortal(
overlayLocation: _getLocation(zOrderIndex, widget._targetRootOverlay),
overlayChild: _DeferredLayout(child: Builder(builder: widget.overlayChildBuilder)),
child: widget.child,
);
}
}
/// A location in an [Overlay].
///
/// An [_OverlayEntryLocation] determines the [Overlay] the associated
/// [OverlayPortal] should put its overlay child onto, as well as the overlay
/// child's paint order in relation to other contents painted on the [Overlay].
//
// An _OverlayEntryLocation is a cursor pointing to a location in a particular
// Overlay's child model, and provides methods to insert/remove/move a
// _RenderDeferredLayoutBox to/from its target _theater.
//
// The occupant (a `RenderBox`) will be painted above the associated
// [OverlayEntry], but below the [OverlayEntry] above that [OverlayEntry].
//
// Additionally, `_activate` and `_deactivate` are called when the overlay
// child's `_OverlayPortalElement` activates/deactivates (for instance, during
// global key reparenting).
// `_OverlayPortalElement` removes its overlay child's render object from the
// target `_RenderTheater` when it deactivates and puts it back on `activated`.
// These 2 methods can be used to "hide" a child in the child model without
// removing it, when the child is expensive/difficult to re-insert at the
// correct location on `activated`.
//
// ### Equality
//
// An `_OverlayEntryLocation` will be used as an Element's slot. These 3 parts
// uniquely identify a place in an overlay's child model:
// - _theater
// - _childModel (the OverlayEntry)
// - _zOrderIndex
//
// Since it can't implement operator== (it's mutable), the same `_OverlayEntryLocation`
// instance must not be used to represent more than one locations.
class _OverlayEntryLocation extends LinkedListEntry<_OverlayEntryLocation> {
_OverlayEntryLocation(this._zOrderIndex, this._childModel, this._theater);
final int _zOrderIndex;
final _OverlayEntryWidgetState _childModel;
final _RenderTheater _theater;
_RenderDeferredLayoutBox? _overlayChildRenderBox;
void _addToChildModel(_RenderDeferredLayoutBox child) {
assert(_overlayChildRenderBox == null, 'Failed to add $child. This location ($this) is already occupied by $_overlayChildRenderBox.');
_overlayChildRenderBox = child;
_childModel._add(this);
_theater.markNeedsPaint();
_theater.markNeedsCompositingBitsUpdate();
_theater.markNeedsSemanticsUpdate();
}
void _removeFromChildModel(_RenderDeferredLayoutBox child) {
assert(child == _overlayChildRenderBox);
_overlayChildRenderBox = null;
assert(_childModel._sortedTheaterSiblings?.contains(this) ?? false);
_childModel._remove(this);
_theater.markNeedsPaint();
_theater.markNeedsCompositingBitsUpdate();
_theater.markNeedsSemanticsUpdate();
}
void _addChild(_RenderDeferredLayoutBox child) {
assert(_debugNotDisposed());
_addToChildModel(child);
_theater._addDeferredChild(child);
assert(child.parent == _theater);
}
void _removeChild(_RenderDeferredLayoutBox child) {
// This call is allowed even when this location is disposed.
_removeFromChildModel(child);
_theater._removeDeferredChild(child);
assert(child.parent == null);
}
void _moveChild(_RenderDeferredLayoutBox child, _OverlayEntryLocation fromLocation) {
assert(fromLocation != this);
assert(_debugNotDisposed());
final _RenderTheater fromTheater = fromLocation._theater;
final _OverlayEntryWidgetState fromModel = fromLocation._childModel;
if (fromTheater != _theater) {
fromTheater._removeDeferredChild(child);
_theater._addDeferredChild(child);
}
if (fromModel != _childModel || fromLocation._zOrderIndex != _zOrderIndex) {
fromLocation._removeFromChildModel(child);
_addToChildModel(child);
}
}
void _activate(_RenderDeferredLayoutBox child) {
assert(_debugNotDisposed());
assert(_overlayChildRenderBox == null, '$_overlayChildRenderBox');
_theater.adoptChild(child);
_overlayChildRenderBox = child;
}
void _deactivate(_RenderDeferredLayoutBox child) {
assert(_debugNotDisposed());
_theater.dropChild(child);
_overlayChildRenderBox = null;
}
bool _debugNotDisposed() {
if (_debugDisposedStackTrace == null) {
return true;
}
throw StateError('$this is already disposed. Stack trace: $_debugDisposedStackTrace');
}
StackTrace? _debugDisposedStackTrace;
@mustCallSuper
void _dispose() {
assert(_debugNotDisposed());
assert(() {
_debugDisposedStackTrace = StackTrace.current;
return true;
}());
}
}
class _RenderTheaterMarker extends InheritedWidget {
const _RenderTheaterMarker({
required this.theater,
required this.overlayEntryWidgetState,
required super.child,
});
final _RenderTheater theater;
final _OverlayEntryWidgetState overlayEntryWidgetState;
@override
bool updateShouldNotify(_RenderTheaterMarker oldWidget) {
return oldWidget.theater != theater
|| oldWidget.overlayEntryWidgetState != overlayEntryWidgetState;
}
static _RenderTheaterMarker? maybeOf(BuildContext context, { bool targetRootOverlay = false }) {
if (targetRootOverlay) {
final InheritedElement? ancestor = _rootRenderTheaterMarkerOf(context.getElementForInheritedWidgetOfExactType<_RenderTheaterMarker>());
assert(ancestor == null || ancestor.widget is _RenderTheaterMarker);
return ancestor != null ? context.dependOnInheritedElement(ancestor) as _RenderTheaterMarker? : null;
}
return context.dependOnInheritedWidgetOfExactType<_RenderTheaterMarker>();
}
static InheritedElement? _rootRenderTheaterMarkerOf(InheritedElement? theaterMarkerElement) {
assert(theaterMarkerElement == null || theaterMarkerElement.widget is _RenderTheaterMarker);
if (theaterMarkerElement == null) {
return null;
}
InheritedElement? ancestor;
theaterMarkerElement.visitAncestorElements((Element element) {
ancestor = element.getElementForInheritedWidgetOfExactType<_RenderTheaterMarker>();
return false;
});
return ancestor == null ? theaterMarkerElement : _rootRenderTheaterMarkerOf(ancestor);
}
}
class _OverlayPortal extends RenderObjectWidget {
/// Creates a widget that renders the given [overlayChild] in the [Overlay]
/// specified by `overlayLocation`.
///
/// The `overlayLocation` parameter must not be null when [overlayChild] is not
/// null.
_OverlayPortal({
required this.overlayLocation,
required this.overlayChild,
required this.child,
}) : assert(overlayChild == null || overlayLocation != null),
assert(overlayLocation == null || overlayLocation._debugNotDisposed());
final Widget? overlayChild;
/// A widget below this widget in the tree.
final Widget? child;
final _OverlayEntryLocation? overlayLocation;
@override
RenderObjectElement createElement() => _OverlayPortalElement(this);
@override
RenderObject createRenderObject(BuildContext context) => _RenderLayoutSurrogateProxyBox();
}
class _OverlayPortalElement extends RenderObjectElement {
_OverlayPortalElement(_OverlayPortal super.widget);
@override
_RenderLayoutSurrogateProxyBox get renderObject => super.renderObject as _RenderLayoutSurrogateProxyBox;
Element? _overlayChild;
Element? _child;
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
final _OverlayPortal widget = this.widget as _OverlayPortal;
_child = updateChild(_child, widget.child, null);
_overlayChild = updateChild(_overlayChild, widget.overlayChild, widget.overlayLocation);
}
@override
void update(_OverlayPortal newWidget) {
super.update(newWidget);
_child = updateChild(_child, newWidget.child, null);
_overlayChild = updateChild(_overlayChild, newWidget.overlayChild, newWidget.overlayLocation);
}
@override
void forgetChild(Element child) {
// The _overlayChild Element does not have a key because the _DeferredLayout
// widget does not take a Key, so only the regular _child can be taken
// during global key reparenting.
assert(child == _child);
_child = null;
super.forgetChild(child);
}
@override
void visitChildren(ElementVisitor visitor) {
final Element? child = _child;
final Element? overlayChild = _overlayChild;
if (child != null) {
visitor(child);
}
if (overlayChild != null) {
visitor(overlayChild);
}
}
@override
void activate() {
super.activate();
final Element? overlayChild = _overlayChild;
if (overlayChild != null) {
final _RenderDeferredLayoutBox? box = overlayChild.renderObject as _RenderDeferredLayoutBox?;
if (box != null) {
assert(!box.attached);
assert(renderObject._deferredLayoutChild == box);
(overlayChild.slot! as _OverlayEntryLocation)._activate(box);
}
}
}
@override
void deactivate() {
final Element? overlayChild = _overlayChild;
// Instead of just detaching the render objects, removing them from the
// render subtree entirely such that if the widget gets reparented to a
// different overlay entry, the overlay child is inserted in the right
// position in the overlay's child list.
//
// This is also a workaround for the !renderObject.attached assert in the
// `RenderObjectElement.deactive()` method.
if (overlayChild != null) {
final _RenderDeferredLayoutBox? box = overlayChild.renderObject as _RenderDeferredLayoutBox?;
if (box != null) {
(overlayChild.slot! as _OverlayEntryLocation)._deactivate(box);
}
}
super.deactivate();
}
@override
void insertRenderObjectChild(RenderBox child, _OverlayEntryLocation? slot) {
assert(child.parent == null, "$child's parent is not null: ${child.parent}");
if (slot != null) {
renderObject._deferredLayoutChild = child as _RenderDeferredLayoutBox;
slot._addChild(child);
} else {
renderObject.child = child;
}
}
// The [_DeferredLayout] widget does not have a key so there will be no
// reparenting between _overlayChild and _child, thus the non-null-typed slots.
@override
void moveRenderObjectChild(_RenderDeferredLayoutBox child, _OverlayEntryLocation oldSlot, _OverlayEntryLocation newSlot) {
assert(newSlot._debugNotDisposed());
newSlot._moveChild(child, oldSlot);
}
@override
void removeRenderObjectChild(RenderBox child, _OverlayEntryLocation? slot) {
if (slot == null) {
renderObject.child = null;
return;
}
assert(renderObject._deferredLayoutChild == child);
slot._removeChild(child as _RenderDeferredLayoutBox);
renderObject._deferredLayoutChild = null;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Element>('child', _child, defaultValue: null));
properties.add(DiagnosticsProperty<Element>('overlayChild', _overlayChild, defaultValue: null));
properties.add(DiagnosticsProperty<Object>('overlayLocation', _overlayChild?.slot, defaultValue: null));
}
}
class _DeferredLayout extends SingleChildRenderObjectWidget {
const _DeferredLayout({
// This widget must not be given a key: we currently do not support
// reparenting between the overlayChild and child.
required Widget child,
}) : super(child: child);
_RenderLayoutSurrogateProxyBox getLayoutParent(BuildContext context) {
return context.findAncestorRenderObjectOfType<_RenderLayoutSurrogateProxyBox>()!;
}
@override
_RenderDeferredLayoutBox createRenderObject(BuildContext context) {
final _RenderLayoutSurrogateProxyBox parent = getLayoutParent(context);
final _RenderDeferredLayoutBox renderObject = _RenderDeferredLayoutBox(parent);
parent._deferredLayoutChild = renderObject;
return renderObject;
}
@override
void updateRenderObject(BuildContext context, _RenderDeferredLayoutBox renderObject) {
assert(renderObject._layoutSurrogate == getLayoutParent(context));
assert(getLayoutParent(context)._deferredLayoutChild == renderObject);
}
}
// A `RenderProxyBox` that defers its layout until its `_layoutSurrogate` is
// laid out.
//
// This `RenderObject` must be a child of a `_RenderTheater`. It guarantees that:
//
// 1. It's a relayout boundary, and `markParentNeedsLayout` is overridden such
// that it never dirties its `_RenderTheater`.
//
// 2. Its `layout` implementation is overridden such that `performLayout` does
// not do anything when its called from `layout`, preventing the parent
// `_RenderTheater` from laying out this subtree prematurely (but this
// `RenderObject` may still be resized). Instead, `markNeedsLayout` will be
// called from within `layout` to schedule a layout update for this relayout
// boundary when needed.
//
// 3. When invoked from `PipelineOwner.flushLayout`, or
// `_layoutSurrogate.performLayout`, this `RenderObject` behaves like an
// `Overlay` that has only one entry.
class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterMixin, LinkedListEntry<_RenderDeferredLayoutBox> {
_RenderDeferredLayoutBox(this._layoutSurrogate);
StackParentData get stackParentData => parentData! as StackParentData;
final _RenderLayoutSurrogateProxyBox _layoutSurrogate;
@override
Iterable<RenderBox> _childrenInPaintOrder() {
final RenderBox? child = this.child;
return child == null
? const Iterable<RenderBox>.empty()
: Iterable<RenderBox>.generate(1, (int i) => child);
}
@override
Iterable<RenderBox> _childrenInHitTestOrder() => _childrenInPaintOrder();
@override
_RenderTheater get theater {
final AbstractNode? parent = this.parent;
return parent is _RenderTheater
? parent
: throw FlutterError('$parent of $this is not a _RenderTheater');
}
@override
void redepthChildren() {
_layoutSurrogate.redepthChild(this);
super.redepthChildren();
}
bool _callingMarkParentNeedsLayout = false;
@override
void markParentNeedsLayout() {
// No re-entrant calls.
if (_callingMarkParentNeedsLayout) {
return;
}
_callingMarkParentNeedsLayout = true;
markNeedsLayout();
_layoutSurrogate.markNeedsLayout();
_callingMarkParentNeedsLayout = false;
}
bool _needsLayout = true;
@override
void markNeedsLayout() {
_needsLayout = true;
super.markNeedsLayout();
}
@override
RenderObject? get debugLayoutParent => _layoutSurrogate;
void layoutByLayoutSurrogate() {
assert(!_parentDoingLayout);
final _RenderTheater? theater = parent as _RenderTheater?;
if (theater == null || !attached) {
assert(false, '$this is not attached to parent');
return;
}
super.layout(BoxConstraints.tight(theater.constraints.biggest));
}
bool _parentDoingLayout = false;
@override
void layout(Constraints constraints, { bool parentUsesSize = false }) {
assert(_needsLayout == debugNeedsLayout);
// Only _RenderTheater calls this implementation.
assert(parent != null);
final bool scheduleDeferredLayout = _needsLayout || this.constraints != constraints;
assert(!_parentDoingLayout);
_parentDoingLayout = true;
super.layout(constraints, parentUsesSize: parentUsesSize);
assert(_parentDoingLayout);
_parentDoingLayout = false;
_needsLayout = false;
assert(!debugNeedsLayout);
if (scheduleDeferredLayout) {
final _RenderTheater parent = this.parent! as _RenderTheater;
// Invoking markNeedsLayout as a layout callback allows this node to be
// merged back to the `PipelineOwner` if it's not already dirty. Otherwise
// this may cause some dirty descendants to performLayout a second time.
parent.invokeLayoutCallback((BoxConstraints constraints) { markNeedsLayout(); });
}
}
@override
void performResize() {
size = constraints.biggest;
}
bool _debugMutationsLocked = false;
@override
void performLayout() {
assert(!_debugMutationsLocked);
if (_parentDoingLayout) {
_needsLayout = false;
return;
}
assert(() {
_debugMutationsLocked = true;
return true;
}());
// This method is directly being invoked from `PipelineOwner.flushLayout`,
// or from `_layoutSurrogate`'s performLayout.
assert(parent != null);
final RenderBox? child = this.child;
if (child == null) {
_needsLayout = false;
return;
}
super.performLayout();
assert(() {
_debugMutationsLocked = false;
return true;
}());
_needsLayout = false;
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
final BoxParentData childParentData = child.parentData! as BoxParentData;
final Offset offset = childParentData.offset;
transform.translate(offset.dx, offset.dy);
}
}
// A RenderProxyBox that makes sure its `deferredLayoutChild` has a greater
// depth than itself.
class _RenderLayoutSurrogateProxyBox extends RenderProxyBox {
_RenderDeferredLayoutBox? _deferredLayoutChild;
@override
void redepthChildren() {
super.redepthChildren();
final _RenderDeferredLayoutBox? child = _deferredLayoutChild;
// If child is not attached, this method will be invoked by child's real
// parent when it's attached.
if (child != null && child.attached) {
assert(child.attached);
redepthChild(child);
}
}
@override
void performLayout() {
super.performLayout();
// Try to layout `_deferredLayoutChild` here now that its configuration
// and constraints are up-to-date. Additionally, during the very first
// layout, this makes sure that _deferredLayoutChild is reachable via tree
// walk.
_deferredLayoutChild?.layoutByLayoutSurrogate();
}
}
...@@ -152,10 +152,11 @@ void main() { ...@@ -152,10 +152,11 @@ void main() {
' AnimatedBuilder\n' ' AnimatedBuilder\n'
' _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#00000]\n' ' _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#00000]\n'
' Semantics\n' ' Semantics\n'
' _RenderTheaterMarker\n'
' _EffectiveTickerMode\n' ' _EffectiveTickerMode\n'
' TickerMode\n' ' TickerMode\n'
' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#00000]\n' ' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#00000]\n'
' _Theatre\n' ' _Theater\n'
' Overlay-[LabeledGlobalKey<OverlayState>#00000]\n' ' Overlay-[LabeledGlobalKey<OverlayState>#00000]\n'
' UnmanagedRestorationScope\n' ' UnmanagedRestorationScope\n'
' _FocusInheritedScope\n' ' _FocusInheritedScope\n'
......
...@@ -454,6 +454,66 @@ void main() { ...@@ -454,6 +454,66 @@ void main() {
})); }));
}); });
testWidgets('The InkWell widget on OverlayPortal does not throw', (WidgetTester tester) async {
final OverlayPortalController controller = OverlayPortalController();
controller.show();
await tester.pumpWidget(
Center(
child: RepaintBoundary(
child: SizedBox.square(
dimension: 200,
child: Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Center(
child: SizedBox.square(
dimension: 100,
// The material partially overlaps the overlayChild.
// This is to verify that the `overlayChild`'s ink
// features aren't clipped by it.
child: Material(
color: Colors.black,
child: OverlayPortal(
controller: controller,
overlayChildBuilder: (BuildContext context) {
return Positioned(
right: 0,
bottom: 0,
child: InkWell(
splashColor: Colors.red,
onTap: () {},
child: const SizedBox.square(dimension: 100),
),
);
},
),
),
),
);
},
),
],
),
),
),
),
),
);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(InkWell)));
addTearDown(() async {
await gesture.up();
});
await tester.pump(); // start gesture
await tester.pump(const Duration(seconds: 2));
expect(tester.takeException(), isNull);
});
testWidgets('Custom rectCallback renders an ink splash from its center', (WidgetTester tester) async { testWidgets('Custom rectCallback renders an ink splash from its center', (WidgetTester tester) async {
const Color splashColor = Color(0xff00ff00); const Color splashColor = Color(0xff00ff00);
......
// 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 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
class _ManyRelayoutBoundaries extends StatelessWidget {
const _ManyRelayoutBoundaries({
required this.levels,
required this.child,
});
final Widget child;
final int levels;
@override
Widget build(BuildContext context) {
final Widget result = levels <= 1
? child
: _ManyRelayoutBoundaries(levels: levels - 1, child: child);
return SizedBox.square(dimension: 50, child: result);
}
}
void rebuildLayoutBuilderSubtree(RenderBox descendant) {
assert(descendant is! RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox>);
AbstractNode? node = descendant.parent;
while (node != null) {
if (node is! RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox>) {
node = node.parent;
} else {
node.markNeedsBuild();
return;
}
}
assert(false);
}
void verifyTreeIsClean() {
final RenderObject renderObject = RendererBinding.instance.renderView;
bool hasDirtyNode = renderObject.debugNeedsLayout;
void visitor(RenderObject renderObject) {
expect(renderObject.debugNeedsLayout, false, reason: '$renderObject is dirty');
hasDirtyNode = hasDirtyNode || renderObject.debugNeedsLayout;
if (!hasDirtyNode) {
renderObject.visitChildren(visitor);
}
}
visitor(renderObject);
}
void verifyOverlayChildReadyForLayout(GlobalKey overlayWidgetKey) {
final RenderBox layoutSurrogate = overlayWidgetKey.currentContext!.findRenderObject()! as RenderBox;
assert(
layoutSurrogate.runtimeType.toString() == '_RenderLayoutSurrogateProxyBox',
layoutSurrogate.runtimeType,
);
if (layoutSurrogate.debugNeedsLayout) {
assert(layoutSurrogate.debugDoingThisLayout);
}
expect(!layoutSurrogate.debugNeedsLayout || layoutSurrogate.debugDoingThisLayout, true);
}
List<RenderObject> _ancestorRenderTheaters(RenderObject child) {
final List<RenderObject> results = <RenderObject>[];
RenderObject? node = child;
while (node != null) {
if (node.runtimeType.toString() == '_RenderTheater') {
results.add(node);
}
final AbstractNode? parent = node.parent;
node = parent is RenderObject? parent : null;
}
return results;
}
void main() {
final OverlayPortalController controller1 = OverlayPortalController(debugLabel: 'controller1');
final OverlayPortalController controller2 = OverlayPortalController(debugLabel: 'controller2');
final OverlayPortalController controller3 = OverlayPortalController(debugLabel: 'controller3');
final OverlayPortalController controller4 = OverlayPortalController(debugLabel: 'controller4');
setUp(() {
controller1.show();
controller2.show();
controller3.show();
controller4.show();
_PaintOrder.paintOrder.clear();
});
testWidgets('The overlay child sees the right inherited widgets', (WidgetTester tester) async {
int buildCount = 0;
TextDirection? directionSeenByOverlayChild;
TextDirection textDirection = TextDirection.rtl;
late StateSetter setState;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return Directionality(
textDirection: textDirection,
child: OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) {
buildCount += 1;
directionSeenByOverlayChild = Directionality.maybeOf(context);
return const SizedBox();
},
child: const SizedBox(),
),
);
}
);
},
),
],
),
),
);
expect(buildCount, 1);
expect(directionSeenByOverlayChild, textDirection);
setState(() {
textDirection = TextDirection.ltr;
});
await tester.pump();
expect(buildCount, 2);
expect(directionSeenByOverlayChild, textDirection);
});
testWidgets('Safe to deactivate and re-activate OverlayPortal', (WidgetTester tester) async {
final Widget widget = Directionality(
key: GlobalKey(debugLabel: 'key'),
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
);
},
),
],
),
);
await tester.pumpWidget(widget);
await tester.pumpWidget(SizedBox(child: widget));
});
testWidgets('Throws when the same controller is attached to multiple OverlayPortal', (WidgetTester tester) async {
final OverlayPortalController controller = OverlayPortalController(debugLabel: 'local controller');
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Column(
children: <Widget>[
OverlayPortal(
controller: controller,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
),
OverlayPortal(
controller: controller,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
),
],
);
},
),
],
),
);
await tester.pumpWidget(widget);
expect(
tester.takeException().toString(),
stringContainsInOrder(<String>['Failed to attach' ,'It is already attached to']),
);
});
testWidgets('show/hide works', (WidgetTester tester) async {
final OverlayPortalController controller = OverlayPortalController(debugLabel: 'local controller');
const Widget target = SizedBox();
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return OverlayPortal(
controller: controller,
overlayChildBuilder: (BuildContext context) => target,
);
},
),
],
),
);
await tester.pumpWidget(widget);
expect(find.byWidget(target), findsNothing);
await tester.pump();
expect(find.byWidget(target), findsNothing);
controller.show();
await tester.pump();
expect(find.byWidget(target), findsOneWidget);
controller.hide();
await tester.pump();
expect(find.byWidget(target), findsNothing);
controller.show();
await tester.pump();
expect(find.byWidget(target), findsOneWidget);
});
testWidgets('overlayChildBuilder is not evaluated until show is called', (WidgetTester tester) async {
final OverlayPortalController controller = OverlayPortalController(debugLabel: 'local controller');
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return OverlayPortal(
controller: controller,
overlayChildBuilder: (BuildContext context) => throw StateError('Unreachable'),
child: const SizedBox(),
);
},
),
],
),
);
await tester.pumpWidget(widget);
expect(tester.takeException(), isNull);
});
testWidgets('overlay child can use Positioned', (WidgetTester tester) async {
double dimensions = 30;
late StateSetter setState;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return Positioned(
width: dimensions,
height: dimensions,
child: const Placeholder(),
);
},
child: const SizedBox(),
);
}
);
},
),
],
),
),
);
expect(tester.getTopLeft(find.byType(Placeholder)), Offset.zero) ;
expect(tester.getSize(find.byType(Placeholder)), const Size(30, 30)) ;
setState(() {
dimensions = 50;
});
await tester.pump();
expect(tester.getTopLeft(find.byType(Placeholder)), Offset.zero) ;
expect(tester.getSize(find.byType(Placeholder)), const Size(50, 50)) ;
});
testWidgets('overlay child can be hit tested', (WidgetTester tester) async {
double offset = 0;
late StateSetter setState;
bool isHit = false;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return Positioned(
left: offset,
top: offset,
width: 1.0,
height: 1.0,
child: GestureDetector(onTap: () { isHit = true; }),
);
},
child: const SizedBox(),
);
}
);
},
),
],
),
),
);
assert(!isHit);
await tester.tapAt(const Offset(0.5, 0.5));
expect(isHit, true);
isHit = false;
setState(() {
offset = 50;
});
await tester.pump();
assert(!isHit);
await tester.tapAt(const Offset(0.5, 0.5));
expect(isHit, false);
isHit = false;
await tester.tapAt(const Offset(50.5, 50.5));
expect(isHit, true);
});
testWidgets('works in a LayoutBuilder', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
);
}
);
},
),
],
),
),
);
expect(tester.takeException(), isNull);
});
testWidgets('works in a LayoutBuilder 2', (WidgetTester tester) async {
late StateSetter setState;
bool shouldShowChild = false;
Widget layoutBuilder(BuildContext context, BoxConstraints constraints) {
return OverlayPortal(
controller: controller2,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
);
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter setter) {
setState = setter;
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: shouldShowChild ? LayoutBuilder(builder: layoutBuilder) : null,
);
}),
],
),
),
);
expect(tester.takeException(), isNull);
setState(() { shouldShowChild = true; });
await tester.pump();
expect(tester.takeException(), isNull);
});
testWidgets('throws when no Overlay', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: SizedBox.square(
dimension: 50,
child: OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
),
),
),
);
expect(
tester.takeException().toString(),
startsWith(
'No Overlay widget found.\n'
'OverlayPortal widgets require an Overlay widget ancestor.\n'
'An overlay lets widgets float on top of other widget children.\n'
'To introduce an Overlay widget, you can either directly include one, or use a widget '
'that contains an Overlay itself, such as a Navigator, WidgetApp, MaterialApp, or CupertinoApp.\n'
'The specific widget that could not find a Overlay ancestor was:\n'
),
);
});
testWidgets('widget is laid out before overlay child', (WidgetTester tester) async {
final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget');
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
int layoutCount = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return _ManyRelayoutBoundaries(levels: 50, child: Builder(builder: (BuildContext context) {
return OverlayPortal(
key: widgetKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
verifyOverlayChildReadyForLayout(widgetKey);
layoutCount += 1;
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}));
}
),
],
),
),
);
expect(layoutCount, 1);
// Make the widget's render object dirty and verifies in the LayoutBuilder's
// callback that the widget's render object is already laid out.
final RenderObject renderChild1 = widgetKey.currentContext!.findRenderObject()!;
renderChild1.markNeedsLayout();
// Dirty both render subtree branches.
childBox.markNeedsLayout();
rebuildLayoutBuilderSubtree(overlayChildBox);
// Make sure childBox's depth is greater than that of the overlay
// child, and childBox's parent isn't dirty (childBox is a dirty relayout
// boundary).
expect(widgetKey.currentContext!.findRenderObject()!.depth, lessThan(overlayChildBox.depth));
await tester.pump();
expect(layoutCount, 2);
verifyTreeIsClean();
});
testWidgets('adding/removing overlay child does not redirty overlay more than once', (WidgetTester tester) async {
final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget');
final GlobalKey overlayKey = GlobalKey(debugLabel: 'overlay');
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final _RenderLayoutCounter overlayLayoutCounter = _RenderLayoutCounter();
int layoutCount = 0;
controller1.hide();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
// Overlay.performLayout will call layoutCounter.layout.
OverlayEntry(builder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: overlayLayoutCounter)),
OverlayEntry(
builder: (BuildContext context) {
return _ManyRelayoutBoundaries(levels: 50, child: Builder(builder: (BuildContext context) {
return OverlayPortal(
key: widgetKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
layoutCount += 1;
expect(tester.renderObject(find.byType(Overlay)).debugNeedsLayout, false);
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}));
}
),
],
),
),
);
expect(layoutCount, 0);
expect(overlayLayoutCounter.layoutCount, 1);
verifyTreeIsClean();
// Add overlay child.
controller1.show();
await tester.pump();
expect(layoutCount, 1);
expect(overlayLayoutCounter.layoutCount, 1);
verifyTreeIsClean();
// Remove the overlay child.
controller1.hide();
await tester.pump();
expect(layoutCount, 1);
expect(overlayLayoutCounter.layoutCount, 1);
verifyTreeIsClean();
});
testWidgets('Change overlay constraints', (WidgetTester tester) async {
final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget outer');
final GlobalKey overlayKey = GlobalKey(debugLabel: 'overlay');
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final _RenderLayoutCounter overlayLayoutCounter = _RenderLayoutCounter();
int layoutCount = 0;
late StateSetter setState;
double dimension = 100;
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: SizedBox.square(
dimension: dimension,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
// Overlay.performLayout calls layoutCounter.layout.
OverlayEntry(builder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: overlayLayoutCounter)),
OverlayEntry(
builder: (BuildContext outerEntryContext) {
return Center(
child: _ManyRelayoutBoundaries(
levels: 50,
child: Builder(builder: (BuildContext context) {
return OverlayPortal(
key: widgetKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
layoutCount += 1;
// Both overlays need to be clean at this point.
expect(
tester.renderObjectList(find.byType(Overlay)),
everyElement(wrapMatcher((RenderObject object) => !object.debugNeedsLayout || object.debugDoingThisLayout)),
);
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}),
),
);
}
),
],
),
),
),
);
}
),
);
expect(layoutCount, 1);
expect(overlayLayoutCounter.layoutCount, 1);
expect(childBox.size, const Size.square(50));
expect(overlayChildBox.size, const Size.square(100));
verifyTreeIsClean();
// The incoming constraints changed.
setState(() {
dimension = 150;
});
await tester.pump();
expect(childBox.size, const Size.square(50));
expect(overlayChildBox.size, const Size.square(150));
expect(layoutCount, 2);
expect(overlayLayoutCounter.layoutCount, 2);
verifyTreeIsClean();
});
testWidgets('Can target the root overlay', (WidgetTester tester) async {
final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget outer');
final GlobalKey rootOverlayKey = GlobalKey(debugLabel: 'root overlay');
final GlobalKey localOverlayKey = GlobalKey(debugLabel: 'local overlay');
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final _RenderLayoutCounter overlayLayoutCounter = _RenderLayoutCounter();
int layoutCount = 0;
OverlayPortal Function({ Widget? child, required OverlayPortalController controller, Key? key, required WidgetBuilder overlayChildBuilder, }) constructorToUse = OverlayPortal.new;
late StateSetter setState;
// This tree has 3 nested Overlays.
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: rootOverlayKey,
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return Overlay(
key: localOverlayKey,
initialEntries: <OverlayEntry>[
// Overlay.performLayout calls layoutCounter.layout.
OverlayEntry(builder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: overlayLayoutCounter)),
OverlayEntry(builder: (BuildContext outerEntryContext) {
return Center(
child: Builder(builder: (BuildContext context) {
return constructorToUse(
key: widgetKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
layoutCount += 1;
// Both overlays need to be clean at this point.
expect(
tester.renderObjectList(find.byType(Overlay)),
everyElement(wrapMatcher((RenderObject object) => !object.debugNeedsLayout || object.debugDoingThisLayout)),
);
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}),
);
}),
],
);
}),
],
);
}),
],
),
),
);
}
),
);
expect(layoutCount, 1);
expect(overlayLayoutCounter.layoutCount, 1);
expect(_ancestorRenderTheaters(overlayChildBox).length, 3);
verifyTreeIsClean();
// Now targets the root overlay.
setState(() { constructorToUse = OverlayPortal.targetsRootOverlay; });
await tester.pump();
expect(layoutCount, 2);
expect(overlayLayoutCounter.layoutCount, 1);
expect(_ancestorRenderTheaters(overlayChildBox).single, tester.renderObject(find.byKey(rootOverlayKey)));
verifyTreeIsClean();
});
group('GlobalKey Reparenting', () {
testWidgets('child is laid out before overlay child after OverlayEntry shuffle', (WidgetTester tester) async {
int layoutCount = 0;
final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget');
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final OverlayEntry overlayEntry1 = OverlayEntry(builder: (BuildContext context) {
return _ManyRelayoutBoundaries(
levels: 50,
child: Builder(builder: (BuildContext context) {
return OverlayPortal(
key: widgetKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
layoutCount += 1;
verifyOverlayChildReadyForLayout(widgetKey);
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}),
);
});
final OverlayEntry overlayEntry2 = OverlayEntry(builder: (BuildContext context) => const Placeholder());
final OverlayEntry overlayEntry3 = OverlayEntry(builder: (BuildContext context) => const Placeholder());
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[overlayEntry1, overlayEntry2, overlayEntry3],
),
),
);
expect(layoutCount, 1);
verifyTreeIsClean();
widgetKey.currentContext!.findRenderObject()!.markNeedsLayout();
childBox.markNeedsLayout();
rebuildLayoutBuilderSubtree(overlayChildBox);
// Make sure childBox's depth is greater than that of the overlay child.
expect(
widgetKey.currentContext!.findRenderObject()!.depth,
lessThan(overlayChildBox.depth),
);
tester.state<OverlayState>(find.byType(Overlay)).rearrange(<OverlayEntry>[overlayEntry3, overlayEntry2, overlayEntry1]);
await tester.pump();
expect(layoutCount, 2);
expect(widgetKey.currentContext!.findRenderObject()!.depth, lessThan(overlayChildBox.depth));
verifyTreeIsClean();
});
testWidgets('widget is laid out before overlay child after reparenting', (WidgetTester tester) async {
final GlobalKey targetGlobalKey = GlobalKey(debugLabel: 'target widget');
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
late StateSetter setState1, setState2;
bool targetMovedToOverlayEntry3 = false;
int layoutCount1 = 0;
int layoutCount2 = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return _ManyRelayoutBoundaries(
levels: 50,
child: StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return targetMovedToOverlayEntry3 ? const SizedBox() : OverlayPortal(
key: targetGlobalKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
layoutCount1 += 1;
verifyOverlayChildReadyForLayout(targetGlobalKey);
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}),
);
}),
OverlayEntry(builder: (BuildContext context) => const Placeholder()),
OverlayEntry(builder: (BuildContext context) {
return SizedBox(
child: StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState2 = stateSetter;
return !targetMovedToOverlayEntry3 ? const SizedBox() : OverlayPortal(
key: targetGlobalKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
layoutCount2 += 1;
verifyOverlayChildReadyForLayout(targetGlobalKey);
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}),
);
}),
],
),
),
);
expect(layoutCount1, 1);
expect(layoutCount2, 0);
targetGlobalKey.currentContext!.findRenderObject()!.markNeedsLayout();
childBox.markNeedsLayout();
rebuildLayoutBuilderSubtree(overlayChildBox);
setState1(() {});
setState2(() {});
targetMovedToOverlayEntry3 = true;
await tester.pump();
expect(
targetGlobalKey.currentContext!.findRenderObject()!.depth,
lessThan(overlayChildBox.depth),
);
verifyTreeIsClean();
expect(layoutCount1, 1);
expect(layoutCount2, 1);
});
testWidgets('Swap child and overlayChild', (WidgetTester tester) async {
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
late StateSetter setState;
bool swapChildAndRemoteChild = false;
// WidgetToRenderBoxAdapter has its own builtin GlobalKey.
final Widget child1 = WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
final Widget child2 = WidgetToRenderBoxAdapter(renderBox: childBox);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return _ManyRelayoutBoundaries(
levels: 50,
child: StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => swapChildAndRemoteChild ? child1 : child2,
child: swapChildAndRemoteChild ? child2 : child1,
);
}),
);
}),
],
),
),
);
setState(() { swapChildAndRemoteChild = true; });
await tester.pump();
verifyTreeIsClean();
});
testWidgets('forgetChild', (WidgetTester tester) async {
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
late StateSetter setState1;
late StateSetter setState2;
bool takeChildren = false;
// WidgetToRenderBoxAdapter has its own builtin GlobalKey.
final Widget child1 = WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
final Widget child2 = WidgetToRenderBoxAdapter(renderBox: childBox);
controller1.hide();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState2 = stateSetter;
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => child2,
child: takeChildren ? child1 : null,
);
});
}),
OverlayEntry(builder: (BuildContext context) {
return _ManyRelayoutBoundaries(
levels: 50,
child: StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return OverlayPortal(
controller: controller2,
overlayChildBuilder: (BuildContext context) => child1,
child: takeChildren ? null : child2,
);
}),
);
}),
],
),
),
);
controller1.show();
controller2.hide();
setState2(() { takeChildren = true; });
setState1(() { });
await tester.pump();
verifyTreeIsClean();
});
testWidgets('Nested overlay children: swap inner and outer', (WidgetTester tester) async {
final GlobalKey outerKey = GlobalKey(debugLabel: 'Original Outer Widget');
final GlobalKey innerKey = GlobalKey(debugLabel: 'Original Inner Widget');
final RenderBox child1Box = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox child2Box = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
late StateSetter setState;
bool swapped = false;
// WidgetToRenderBoxAdapter has its own builtin GlobalKey.
final Widget child1 = WidgetToRenderBoxAdapter(renderBox: child1Box);
final Widget child2 = WidgetToRenderBoxAdapter(renderBox: child2Box);
final Widget child3 = WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return OverlayPortal(
key: swapped ? outerKey : innerKey,
controller: swapped ? controller2 : controller1,
overlayChildBuilder: (BuildContext context) {
return OverlayPortal(
key: swapped ? innerKey : outerKey,
controller: swapped ? controller1 : controller2,
overlayChildBuilder: (BuildContext context) {
return OverlayPortal(
controller: OverlayPortalController(),
overlayChildBuilder: (BuildContext context) => child3,
);
},
child: child2,
);
},
child: child1,
);
});
}),
],
),
),
);
setState(() { swapped = true; });
await tester.pump();
verifyTreeIsClean();
});
testWidgets('Paint order', (WidgetTester tester) async {
final GlobalKey outerKey = GlobalKey(debugLabel: 'Original Outer Widget');
final GlobalKey innerKey = GlobalKey(debugLabel: 'Original Inner Widget');
late StateSetter setState;
const Widget child1 = _PaintOrder();
const Widget child2 = _PaintOrder();
const Widget child3 = _PaintOrder();
const Widget child4 = _PaintOrder();
controller1.show();
controller2.show();
controller3.show();
controller4.show();
// Expected Order child1 -> innerKey -> child4.
Widget widget = Column(
children: <Widget>[
OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => child1,
),
OverlayPortal(
key: outerKey,
controller: controller2,
overlayChildBuilder: (BuildContext context) {
return OverlayPortal(
key: innerKey,
controller: controller3,
overlayChildBuilder: (BuildContext context) => child3,
child: child2,
);
},
),
OverlayPortal(
controller: controller4,
overlayChildBuilder: (BuildContext context) => child4,
),
],
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return widget;
});
}),
],
),
),
);
expect(_PaintOrder.paintOrder,
<Widget>[
child1,
child2,
child3,
child4,
],
);
_PaintOrder.paintOrder.clear();
// Swap the nested OverlayPortal.
widget = Column(
children: <Widget>[
OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => child1,
),
OverlayPortal(
key: innerKey,
controller: controller3,
overlayChildBuilder: (BuildContext context) {
return OverlayPortal(
key: outerKey,
controller: controller2,
overlayChildBuilder: (BuildContext context) => child3,
child: child2,
);
},
),
OverlayPortal(
controller: controller4,
overlayChildBuilder: (BuildContext context) => child4,
),
],
);
setState(() {});
await tester.pump();
expect(_PaintOrder.paintOrder,
<Widget>[
child1,
child3,
child2,
child4,
],
);
});
group('Swapping', () {
StateSetter? setState1, setState2;
bool swapped = false;
void setState({ required bool newValue }) {
swapped = newValue;
setState1?.call(() {});
setState2?.call(() {});
}
tearDown(() {
swapped = false;
setState1 = null;
setState2 = null;
});
testWidgets('between OverlayEntry & overlayChild', (WidgetTester tester) async {
final _RenderLayoutCounter counter1 = _RenderLayoutCounter();
final _RenderLayoutCounter counter2 = _RenderLayoutCounter();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
// WidgetToRenderBoxAdapter is keyed by the render box.
return WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1);
}),
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState2 = stateSetter;
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter1 : counter2),
child: const SizedBox(),
);
}),
],
),
),
);
expect(counter1.layoutCount, 1);
expect(counter2.layoutCount, 1);
setState(newValue: true);
await tester.pump();
expect(counter1.layoutCount, 2);
expect(counter2.layoutCount, 2);
setState(newValue: false);
await tester.pump();
expect(counter1.layoutCount, 3);
expect(counter2.layoutCount, 3);
});
testWidgets('between OverlayEntry & overlayChild, featuring LayoutBuilder', (WidgetTester tester) async {
final _RenderLayoutCounter counter1 = _RenderLayoutCounter();
final _RenderLayoutCounter counter2 = _RenderLayoutCounter();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1);
}),
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState2 = stateSetter;
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter1 : counter2),
child: const SizedBox(),
);
}
);
}),
],
),
),
);
expect(counter1.layoutCount, 1);
expect(counter2.layoutCount, 1);
setState(newValue: true);
await tester.pump();
expect(counter1.layoutCount, 2);
expect(counter2.layoutCount, 2);
setState(newValue: false);
await tester.pump();
expect(counter1.layoutCount, 3);
expect(counter2.layoutCount, 3);
});
testWidgets('between overlayChild & overlayChild', (WidgetTester tester) async {
final _RenderLayoutCounter counter1 = _RenderLayoutCounter();
final _RenderLayoutCounter counter2 = _RenderLayoutCounter();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return OverlayPortal(
// WidgetToRenderBoxAdapter is keyed by the render box.
controller: controller1,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1),
child: const SizedBox(),
);
}),
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState2 = stateSetter;
return OverlayPortal(
controller: controller2,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter1 : counter2),
child: const SizedBox(),
);
}),
],
),
),
);
expect(counter1.layoutCount, 1);
expect(counter2.layoutCount, 1);
setState(newValue: true);
await tester.pump();
expect(counter1.layoutCount, 2);
expect(counter2.layoutCount, 2);
setState(newValue: false);
await tester.pump();
expect(counter1.layoutCount, 3);
expect(counter2.layoutCount, 3);
});
testWidgets('between overlayChild & overlayChild, featuring LayoutBuilder', (WidgetTester tester) async {
final _RenderLayoutCounter counter1 = _RenderLayoutCounter();
final _RenderLayoutCounter counter2 = _RenderLayoutCounter();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1),
child: const SizedBox(),
);
}
);
}),
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState2 = stateSetter;
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return OverlayPortal(
controller: controller2,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter1 : counter2),
child: const SizedBox(),
);
}
);
}),
],
),
),
);
expect(counter1.layoutCount, 1);
expect(counter2.layoutCount, 1);
setState(newValue: true);
await tester.pump();
expect(counter1.layoutCount, 2);
expect(counter2.layoutCount, 2);
setState(newValue: false);
await tester.pump();
expect(counter1.layoutCount, 3);
expect(counter2.layoutCount, 3);
});
testWidgets('between child & overlayChild', (WidgetTester tester) async {
final _RenderLayoutCounter counter1 = _RenderLayoutCounter();
final _RenderLayoutCounter counter2 = _RenderLayoutCounter();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return OverlayPortal(
// WidgetToRenderBoxAdapter is keyed by the render box.
controller: controller1,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1),
child: WidgetToRenderBoxAdapter(renderBox: swapped ? counter1 : counter2),
);
}),
],
),
),
);
expect(counter1.layoutCount, 1);
expect(counter2.layoutCount, 1);
setState(newValue: true);
await tester.pump();
expect(counter1.layoutCount, 2);
expect(counter2.layoutCount, 2);
setState(newValue: false);
await tester.pump();
expect(counter1.layoutCount, 3);
expect(counter2.layoutCount, 3);
});
testWidgets('between child & overlayChild, featuring LayoutBuilder', (WidgetTester tester) async {
final _RenderLayoutCounter counter1 = _RenderLayoutCounter();
final _RenderLayoutCounter counter2 = _RenderLayoutCounter();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return OverlayPortal(
// WidgetToRenderBoxAdapter is keyed by the render box.
controller: controller1,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1),
child: WidgetToRenderBoxAdapter(renderBox: swapped ? counter1 : counter2),
);
}
);
}),
],
),
),
);
expect(counter1.layoutCount, 1);
expect(counter2.layoutCount, 1);
setState(newValue: true);
await tester.pump();
expect(counter1.layoutCount, 2);
expect(counter2.layoutCount, 2);
setState(newValue: false);
await tester.pump();
expect(counter1.layoutCount, 3);
expect(counter2.layoutCount, 3);
});
});
});
group('Paint order', () {
testWidgets('show bringsToTop', (WidgetTester tester) async {
controller1.hide();
const _PaintOrder child1 = _PaintOrder();
const _PaintOrder child2 = _PaintOrder();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return Column(
children: <Widget>[
OverlayPortal(controller: controller1, overlayChildBuilder: (BuildContext context) => child1),
OverlayPortal(controller: controller2, overlayChildBuilder: (BuildContext context) => child2),
],
);
}),
],
),
),
);
// Only child2 is visible.
expect(
_PaintOrder.paintOrder,
<_PaintOrder>[
child2,
],
);
_PaintOrder.paintOrder.clear();
controller1.show();
await tester.pump();
expect(
_PaintOrder.paintOrder,
<_PaintOrder>[
child2,
child1,
],
);
_PaintOrder.paintOrder.clear();
controller2.show();
await tester.pump();
expect(
_PaintOrder.paintOrder,
<_PaintOrder>[
child1,
child2,
],
);
_PaintOrder.paintOrder.clear();
controller2.hide();
controller1.hide();
await tester.pump();
expect(
_PaintOrder.paintOrder,
isEmpty,
);
});
testWidgets('Paint order does not change after global key reparenting', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
late StateSetter setState;
bool reparented = false;
// WidgetToRenderBoxAdapter has its own builtin GlobalKey.
final RenderBox child1Box = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox child2Box = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final Widget child1 = WidgetToRenderBoxAdapter(renderBox: child1Box);
final Widget child2 = WidgetToRenderBoxAdapter(renderBox: child2Box);
final Widget overlayPortal1 = OverlayPortal(
key: key,
controller: controller1,
overlayChildBuilder: (BuildContext context) => child1,
child: const SizedBox(),
);
final Widget overlayPortal2 = OverlayPortal(
controller: controller2,
overlayChildBuilder: (BuildContext context) => child2,
child: const SizedBox(),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return Column(
children: <Widget>[
StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return reparented ? SizedBox(child: overlayPortal1) : overlayPortal1;
}),
overlayPortal2,
],
);
}),
],
),
),
);
final RenderObject theater = tester.renderObject<RenderObject>(find.byType(Overlay));
final List<RenderObject> childrenVisited = <RenderObject>[];
theater.visitChildren(childrenVisited.add);
expect(childrenVisited.length, 3);
expect(childrenVisited, containsAllInOrder(<AbstractNode>[child1Box.parent!, child2Box.parent!]));
childrenVisited.clear();
setState(() { reparented = true; });
await tester.pump();
theater.visitChildren(childrenVisited.add);
// The child list stays the same.
expect(childrenVisited, containsAllInOrder(<AbstractNode>[child1Box.parent!, child2Box.parent!]));
});
});
}
class OverlayStatefulEntry extends OverlayEntry {
OverlayStatefulEntry({
required StatefulWidgetBuilder builder,
}) : super(builder: (BuildContext context) => StatefulBuilder(builder: builder));
}
class _RenderLayoutCounter extends RenderProxyBox {
int layoutCount = 0;
bool _parentDoingLayout = false;
@override
void layout(Constraints constraints, {bool parentUsesSize = false}) {
assert(!_parentDoingLayout);
_parentDoingLayout = true;
layoutCount += 1;
super.layout(constraints, parentUsesSize: parentUsesSize);
_parentDoingLayout = false;
}
@override
void performLayout() {
super.performLayout();
if (!_parentDoingLayout) {
layoutCount += 1;
}
}
}
class _PaintOrder extends SingleChildRenderObjectWidget {
const _PaintOrder();
static List<_PaintOrder> paintOrder = <_PaintOrder>[];
void onPaint() => paintOrder.add(this);
@override
_RenderPaintRecorder createRenderObject(BuildContext context) => _RenderPaintRecorder()..onPaint = onPaint;
@override
void updateRenderObject(BuildContext context, _RenderPaintRecorder renderObject) => renderObject.onPaint = onPaint;
}
class _RenderPaintRecorder extends RenderProxyBox {
VoidCallback? onPaint;
@override
void paint(PaintingContext context, Offset offset) {
onPaint?.call();
super.paint(context, offset);
}
}
...@@ -41,7 +41,7 @@ void main() { ...@@ -41,7 +41,7 @@ void main() {
expect( expect(
theater.toStringDeep(minLevel: DiagnosticLevel.info), theater.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'_RenderTheatre#744c9\n' '_RenderTheater#744c9\n'
' │ parentData: <none>\n' ' │ parentData: <none>\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n' ' │ size: Size(800.0, 600.0)\n'
...@@ -114,7 +114,7 @@ void main() { ...@@ -114,7 +114,7 @@ void main() {
expect( expect(
theater.toStringDeep(minLevel: DiagnosticLevel.info), theater.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'_RenderTheatre#385b3\n' '_RenderTheater#385b3\n'
' │ parentData: <none>\n' ' │ parentData: <none>\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n' ' │ size: Size(800.0, 600.0)\n'
......
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