Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
e7ab3b07
Unverified
Commit
e7ab3b07
authored
Mar 02, 2023
by
LongCatIsLooong
Committed by
GitHub
Mar 02, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
`OverlayPortal` (#105335)
`OverlayPortal`
parent
60de2aa9
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
3066 additions
and
132 deletions
+3066
-132
overlay_portal.0.dart
examples/api/lib/widgets/overlay/overlay_portal.0.dart
+59
-0
overlay_portal.0_test.dart
examples/api/test/widgets/overlay/overlay_portal.0_test.dart
+44
-0
material.dart
packages/flutter/lib/src/material/material.dart
+65
-24
object.dart
packages/flutter/lib/src/rendering/object.dart
+26
-9
framework.dart
packages/flutter/lib/src/widgets/framework.dart
+1
-0
overlay.dart
packages/flutter/lib/src/widgets/overlay.dart
+1220
-96
debug_test.dart
packages/flutter/test/material/debug_test.dart
+2
-1
ink_paint_test.dart
packages/flutter/test/material/ink_paint_test.dart
+60
-0
overlay_portal_test.dart
packages/flutter/test/widgets/overlay_portal_test.dart
+1587
-0
overlay_test.dart
packages/flutter/test/widgets/overlay_test.dart
+2
-2
No files found.
examples/api/lib/widgets/overlay/overlay_portal.0.dart
0 → 100644
View file @
e7ab3b07
// 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'
),
),
),
);
}
}
examples/api/test/widgets/overlay/overlay_portal.0_test.dart
0 → 100644
View file @
e7ab3b07
// 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
);
});
}
packages/flutter/lib/src/material/material.dart
View file @
e7ab3b07
...
@@ -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.
...
...
packages/flutter/lib/src/rendering/object.dart
View file @
e7ab3b07
...
@@ -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
.
p
arent
;
final
AbstractNode
?
p
=
activeLayoutRoot
.
debugLayoutP
arent
;
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
...
...
packages/flutter/lib/src/widgets/framework.dart
View file @
e7ab3b07
...
@@ -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
;
...
...
packages/flutter/lib/src/widgets/overlay.dart
View file @
e7ab3b07
...
@@ -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
=>
_overlay
StateMounted
.
value
;
bool
get
mounted
=>
_overlay
EntryStateNotifier
.
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
);
_overlay
StateMounted
.
addListener
(
listener
);
_overlay
EntryStateNotifier
.
addListener
(
listener
);
}
}
@override
@override
void
removeListener
(
VoidCallback
listener
)
{
void
removeListener
(
VoidCallback
listener
)
{
_overlay
StateMounted
.
removeListener
(
listener
);
_overlay
EntryStateNotifier
.
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
)
{
_overlay
StateMounted
.
dispose
();
_overlay
EntryStateNotifier
.
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
)
{
_overlay
StateMounted
.
dispose
();
_overlay
EntryStateNotifier
.
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
.
_overlay
StateMounted
.
value
=
false
;
widget
.
entry
.
_overlay
EntryStateNotifier
.
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
=
<
_OverlayEntry
Widget
>[];
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
_Theat
re
(
return
_Theat
er
(
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
_Theat
re
extends
MultiChildRenderObjectWidget
{
class
_Theat
er
extends
MultiChildRenderObjectWidget
{
const
_Theat
re
({
const
_Theat
er
({
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
_Theat
reElement
createElement
()
=>
_Theatre
Element
(
this
);
_Theat
erElement
createElement
()
=>
_Theater
Element
(
this
);
@override
@override
_RenderTheat
re
createRenderObject
(
BuildContext
context
)
{
_RenderTheat
er
createRenderObject
(
BuildContext
context
)
{
return
_RenderTheat
re
(
return
_RenderTheat
er
(
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
,
_RenderTheat
re
renderObject
)
{
void
updateRenderObject
(
BuildContext
context
,
_RenderTheat
er
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
_Theat
re
Element
extends
MultiChildRenderObjectElement
{
class
_Theat
er
Element
extends
MultiChildRenderObjectElement
{
_Theat
reElement
(
_Theatre
super
.
widget
);
_Theat
erElement
(
_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
_Theat
re
theatre
=
widget
as
_Theatre
;
final
_Theat
er
theater
=
widget
as
_Theater
;
assert
(
children
.
length
>=
theat
re
.
skipCount
);
assert
(
children
.
length
>=
theat
er
.
skipCount
);
children
.
skip
(
theat
re
.
skipCount
).
forEach
(
visitor
);
children
.
skip
(
theat
er
.
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
!
Stack
ParentData
)
{
if
(
child
.
parentData
is
!
_Theater
ParentData
)
{
child
.
parentData
=
Stack
ParentData
();
child
.
parentData
=
_Theater
ParentData
();
}
}
}
}
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
();
}
}
packages/flutter/test/material/debug_test.dart
View file @
e7ab3b07
...
@@ -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
'
' _Theat
re
\n
'
' _Theat
er
\n
'
' Overlay-[LabeledGlobalKey<OverlayState>#00000]
\n
'
' Overlay-[LabeledGlobalKey<OverlayState>#00000]
\n
'
' UnmanagedRestorationScope
\n
'
' UnmanagedRestorationScope
\n
'
' _FocusInheritedScope
\n
'
' _FocusInheritedScope
\n
'
...
...
packages/flutter/test/material/ink_paint_test.dart
View file @
e7ab3b07
...
@@ -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
);
...
...
packages/flutter/test/widgets/overlay_portal_test.dart
0 → 100644
View file @
e7ab3b07
// 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
);
}
}
packages/flutter/test/widgets/overlay_test.dart
View file @
e7ab3b07
...
@@ -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
(
'_RenderTheat
re
#744c9
\n
'
'_RenderTheat
er
#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
(
'_RenderTheat
re
#385b3
\n
'
'_RenderTheat
er
#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
'
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment