blob: c789775ef5155b1fbf864e5f8f51f69197a69acf [file] [log] [blame]
// 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 'dart:collection';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'basic.dart';
import 'framework.dart';
import 'lookup_boundary.dart';
import 'ticker_provider.dart';
// Examples can assume:
// late BuildContext context;
// * OverlayEntry Implementation
/// A place in an [Overlay] that can contain a widget.
///
/// Overlay entries are inserted into an [Overlay] using the
/// [OverlayState.insert] or [OverlayState.insertAll] functions. To find the
/// closest enclosing overlay for a given [BuildContext], use the [Overlay.of]
/// function.
///
/// An overlay entry can be in at most one overlay at a time. To remove an entry
/// from its overlay, call the [remove] function on the overlay entry.
///
/// Because an [Overlay] uses a [Stack] layout, overlay entries can use
/// [Positioned] and [AnimatedPositioned] to position themselves within the
/// overlay.
///
/// For example, [Draggable] uses an [OverlayEntry] to show the drag avatar that
/// follows the user's finger across the screen after the drag begins. Using the
/// overlay to display the drag avatar lets the avatar float over the other
/// widgets in the app. As the user's finger moves, draggable calls
/// [markNeedsBuild] on the overlay entry to cause it to rebuild. In its build,
/// the entry includes a [Positioned] with its top and left property set to
/// position the drag avatar near the user's finger. When the drag is over,
/// [Draggable] removes the entry from the overlay to remove the drag avatar
/// from view.
///
/// By default, if there is an entirely [opaque] entry over this one, then this
/// one will not be included in the widget tree (in particular, stateful widgets
/// within the overlay entry will not be instantiated). To ensure that your
/// overlay entry is still built even if it is not visible, set [maintainState]
/// to true. This is more expensive, so should be done with care. In particular,
/// if widgets in an overlay entry with [maintainState] set to true repeatedly
/// call [State.setState], the user's battery will be drained unnecessarily.
///
/// [OverlayEntry] is a [Listenable] that notifies when the widget built by
/// [builder] is mounted or unmounted, whose exact state can be queried by
/// [mounted]. After the owner of the [OverlayEntry] calls [remove] and then
/// [dispose], the widget may not be immediately removed from the widget tree.
/// As a result listeners of the [OverlayEntry] can get notified for one last
/// time after the [dispose] call, when the widget is eventually unmounted.
///
/// See also:
///
/// * [Overlay]
/// * [OverlayState]
/// * [WidgetsApp]
/// * [MaterialApp]
class OverlayEntry implements Listenable {
/// Creates an overlay entry.
///
/// To insert the entry into an [Overlay], first find the overlay using
/// [Overlay.of] and then call [OverlayState.insert]. To remove the entry,
/// call [remove] on the overlay entry itself.
OverlayEntry({
required this.builder,
bool opaque = false,
bool maintainState = false,
}) : _opaque = opaque,
_maintainState = maintainState;
/// This entry will include the widget built by this builder in the overlay at
/// the entry's position.
///
/// To cause this builder to be called again, call [markNeedsBuild] on this
/// overlay entry.
final WidgetBuilder builder;
/// Whether this entry occludes the entire overlay.
///
/// If an entry claims to be opaque, then, for efficiency, the overlay will
/// skip building entries below that entry unless they have [maintainState]
/// set.
bool get opaque => _opaque;
bool _opaque;
set opaque(bool value) {
assert(!_disposedByOwner);
if (_opaque == value) {
return;
}
_opaque = value;
_overlay?._didChangeEntryOpacity();
}
/// Whether this entry must be included in the tree even if there is a fully
/// [opaque] entry above it.
///
/// By default, if there is an entirely [opaque] entry over this one, then this
/// one will not be included in the widget tree (in particular, stateful widgets
/// within the overlay entry will not be instantiated). To ensure that your
/// overlay entry is still built even if it is not visible, set [maintainState]
/// to true. This is more expensive, so should be done with care. In particular,
/// if widgets in an overlay entry with [maintainState] set to true repeatedly
/// call [State.setState], the user's battery will be drained unnecessarily.
///
/// This is used by the [Navigator] and [Route] objects to ensure that routes
/// are kept around even when in the background, so that [Future]s promised
/// from subsequent routes will be handled properly when they complete.
bool get maintainState => _maintainState;
bool _maintainState;
set maintainState(bool value) {
assert(!_disposedByOwner);
if (_maintainState == value) {
return;
}
_maintainState = value;
assert(_overlay != null);
_overlay!._didChangeEntryOpacity();
}
/// Whether the [OverlayEntry] is currently mounted in the widget tree.
///
/// The [OverlayEntry] notifies its listeners when this value changes.
bool get mounted => _overlayEntryStateNotifier.value != null;
/// The currently mounted `_OverlayEntryWidgetState` built using this [OverlayEntry].
final ValueNotifier<_OverlayEntryWidgetState?> _overlayEntryStateNotifier = ValueNotifier<_OverlayEntryWidgetState?>(null);
@override
void addListener(VoidCallback listener) {
assert(!_disposedByOwner);
_overlayEntryStateNotifier.addListener(listener);
}
@override
void removeListener(VoidCallback listener) {
_overlayEntryStateNotifier.removeListener(listener);
}
OverlayState? _overlay;
final GlobalKey<_OverlayEntryWidgetState> _key = GlobalKey<_OverlayEntryWidgetState>();
/// Remove this entry from the overlay.
///
/// This should only be called once.
///
/// 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
/// 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
/// that if you do call this after the overlay rebuild, the UI will not update
/// until the next frame (i.e. many milliseconds later).
void remove() {
assert(_overlay != null);
assert(!_disposedByOwner);
final OverlayState overlay = _overlay!;
_overlay = null;
if (!overlay.mounted) {
return;
}
overlay._entries.remove(this);
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
overlay._markDirty();
});
} else {
overlay._markDirty();
}
}
/// Cause this entry to rebuild during the next pipeline flush.
///
/// You need to call this function if the output of [builder] has changed.
void markNeedsBuild() {
assert(!_disposedByOwner);
_key.currentState?._markNeedsBuild();
}
void _didUnmount() {
assert(!mounted);
if (_disposedByOwner) {
_overlayEntryStateNotifier.dispose();
}
}
bool _disposedByOwner = false;
/// Discards any resources used by this [OverlayEntry].
///
/// This method must be called after [remove] if the [OverlayEntry] is
/// inserted into an [Overlay].
///
/// After this is called, the object is not in a usable state and should be
/// discarded (calls to [addListener] will throw after the object is disposed).
/// However, the listeners registered may not be immediately released until
/// the widget built using this [OverlayEntry] is unmounted from the widget
/// tree.
///
/// This method should only be called by the object's owner.
void dispose() {
assert(!_disposedByOwner);
assert(_overlay == null, 'An OverlayEntry must first be removed from the Overlay before dispose is called.');
_disposedByOwner = true;
if (!mounted) {
_overlayEntryStateNotifier.dispose();
}
}
@override
String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)';
}
class _OverlayEntryWidget extends StatefulWidget {
const _OverlayEntryWidget({
required Key key,
required this.entry,
required this.overlayState,
this.tickerEnabled = true,
}) : super(key: key);
final OverlayEntry entry;
final OverlayState overlayState;
final bool tickerEnabled;
@override
_OverlayEntryWidgetState createState() => _OverlayEntryWidgetState();
}
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
void initState() {
super.initState();
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
void dispose() {
widget.entry._overlayEntryStateNotifier.value = null;
widget.entry._didUnmount();
_sortedTheaterSiblings = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return TickerMode(
enabled: widget.tickerEnabled,
child: _RenderTheaterMarker(
theater: _theater,
overlayEntryWidgetState: this,
child: widget.entry.builder(context),
),
);
}
void _markNeedsBuild() {
setState(() { /* the state that changed is in the builder */ });
}
}
/// A stack of entries that can be managed independently.
///
/// Overlays let independent child widgets "float" visual elements on top of
/// other widgets by inserting them into the overlay's stack. The overlay lets
/// each of these widgets manage their participation in the overlay using
/// [OverlayEntry] objects.
///
/// Although you can create an [Overlay] directly, it's most common to use the
/// overlay created by the [Navigator] in a [WidgetsApp], [CupertinoApp] or a
/// [MaterialApp]. The navigator uses its overlay to manage the visual
/// appearance of its routes.
///
/// The [Overlay] widget uses a custom stack implementation, which is very
/// similar to the [Stack] widget. The main use case of [Overlay] is related to
/// navigation and being able to insert widgets on top of the pages in an app.
/// For layout purposes unrelated to navigation, consider using [Stack] instead.
///
/// An [Overlay] widget requires a [Directionality] widget to be in scope, so
/// that it can resolve direction-sensitive coordinates of any
/// [Positioned.directional] children.
///
/// {@tool dartpad}
/// This example shows how to use the [Overlay] to highlight the [NavigationBar]
/// destination.
///
/// ** See code in examples/api/lib/widgets/overlay/overlay.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [OverlayEntry], the class that is used for describing the overlay entries.
/// * [OverlayState], which is used to insert the entries into the overlay.
/// * [WidgetsApp], which inserts an [Overlay] widget indirectly via its [Navigator].
/// * [MaterialApp], which inserts an [Overlay] widget indirectly via its [Navigator].
/// * [CupertinoApp], which inserts an [Overlay] widget indirectly via its [Navigator].
/// * [Stack], which allows directly displaying a stack of widgets.
class Overlay extends StatefulWidget {
/// Creates an overlay.
///
/// The initial entries will be inserted into the overlay when its associated
/// [OverlayState] is initialized.
///
/// Rather than creating an overlay, consider using the overlay that is
/// created by the [Navigator] in a [WidgetsApp], [CupertinoApp], or a
/// [MaterialApp] for the application.
const Overlay({
super.key,
this.initialEntries = const <OverlayEntry>[],
this.clipBehavior = Clip.hardEdge,
});
/// The entries to include in the overlay initially.
///
/// These entries are only used when the [OverlayState] is initialized. If you
/// are providing a new [Overlay] description for an overlay that's already in
/// the tree, then the new entries are ignored.
///
/// To add entries to an [Overlay] that is already in the tree, use
/// [Overlay.of] to obtain the [OverlayState] (or assign a [GlobalKey] to the
/// [Overlay] widget and obtain the [OverlayState] via
/// [GlobalKey.currentState]), and then use [OverlayState.insert] or
/// [OverlayState.insertAll].
///
/// To remove an entry from an [Overlay], use [OverlayEntry.remove].
final List<OverlayEntry> initialEntries;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge], and must not be null.
final Clip clipBehavior;
/// The [OverlayState] from the closest instance of [Overlay] that encloses
/// the given context within the closest [LookupBoundary], and, in debug mode,
/// will throw if one is not found.
///
/// In debug mode, if the `debugRequiredFor` argument is provided and an
/// overlay isn't found, then this function will throw an exception containing
/// the runtime type of the given widget in the error message. The exception
/// attempts to explain that the calling [Widget] (the one given by the
/// `debugRequiredFor` argument) needs an [Overlay] to be present to function.
/// If `debugRequiredFor` is not supplied, then the error message is more
/// generic.
///
/// Typical usage is as follows:
///
/// ```dart
/// OverlayState overlay = Overlay.of(context);
/// ```
///
/// If `rootOverlay` is set to true, the state from the furthest instance of
/// this class is given instead. Useful for installing overlay entries above
/// all subsequent instances of [Overlay].
///
/// This method can be expensive (it walks the element tree).
///
/// See also:
///
/// * [Overlay.maybeOf] for a similar function that returns null if an
/// [Overlay] is not found.
static OverlayState of(
BuildContext context, {
bool rootOverlay = false,
Widget? debugRequiredFor,
}) {
final OverlayState? result = maybeOf(context, rootOverlay: rootOverlay);
assert(() {
if (result == null) {
final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorStateOfType<OverlayState>(context);
final List<DiagnosticsNode> information = <DiagnosticsNode>[
ErrorSummary('No Overlay widget found${hiddenByBoundary ? ' within the closest LookupBoundary' : ''}.'),
if (hiddenByBoundary)
ErrorDescription(
'There is an ancestor Overlay widget, but it is hidden by a LookupBoundary.'
),
ErrorDescription('${debugRequiredFor?.runtimeType ?? 'Some'} widgets require an Overlay widget ancestor for correct operation.'),
ErrorHint('The most common way to add an Overlay to an application is to include a MaterialApp, CupertinoApp or Navigator widget in the runApp() call.'),
if (debugRequiredFor != null) DiagnosticsProperty<Widget>('The specific widget that failed to find an overlay was', debugRequiredFor, style: DiagnosticsTreeStyle.errorProperty),
if (context.widget != debugRequiredFor)
context.describeElement('The context from which that widget was searching for an overlay was'),
];
throw FlutterError.fromParts(information);
}
return true;
}());
return result!;
}
/// The [OverlayState] from the closest instance of [Overlay] that encloses
/// the given context within the closest [LookupBoundary], if any.
///
/// Typical usage is as follows:
///
/// ```dart
/// OverlayState? overlay = Overlay.maybeOf(context);
/// ```
///
/// If `rootOverlay` is set to true, the state from the furthest instance of
/// this class is given instead. Useful for installing overlay entries above
/// all subsequent instances of [Overlay].
///
/// This method can be expensive (it walks the element tree).
///
/// See also:
///
/// * [Overlay.of] for a similar function that returns a non-nullable result
/// and throws if an [Overlay] is not found.
static OverlayState? maybeOf(
BuildContext context, {
bool rootOverlay = false,
}) {
return rootOverlay
? LookupBoundary.findRootAncestorStateOfType<OverlayState>(context)
: LookupBoundary.findAncestorStateOfType<OverlayState>(context);
}
@override
OverlayState createState() => OverlayState();
}
/// The current state of an [Overlay].
///
/// Used to insert [OverlayEntry]s into the overlay using the [insert] and
/// [insertAll] functions.
class OverlayState extends State<Overlay> with TickerProviderStateMixin {
final List<OverlayEntry> _entries = <OverlayEntry>[];
@override
void initState() {
super.initState();
insertAll(widget.initialEntries);
}
int _insertionIndex(OverlayEntry? below, OverlayEntry? above) {
assert(above == null || below == null);
if (below != null) {
return _entries.indexOf(below);
}
if (above != null) {
return _entries.indexOf(above) + 1;
}
return _entries.length;
}
/// Insert the given entry into the overlay.
///
/// If `below` is non-null, the entry is inserted just below `below`.
/// If `above` is non-null, the entry is inserted just above `above`.
/// Otherwise, the entry is inserted on top.
///
/// It is an error to specify both `above` and `below`.
void insert(OverlayEntry entry, { OverlayEntry? below, OverlayEntry? above }) {
assert(_debugVerifyInsertPosition(above, below));
assert(!_entries.contains(entry), 'The specified entry is already present in the Overlay.');
assert(entry._overlay == null, 'The specified entry is already present in another Overlay.');
entry._overlay = this;
setState(() {
_entries.insert(_insertionIndex(below, above), entry);
});
}
/// Insert all the entries in the given iterable.
///
/// If `below` is non-null, the entries are inserted just below `below`.
/// If `above` is non-null, the entries are inserted just above `above`.
/// Otherwise, the entries are inserted on top.
///
/// It is an error to specify both `above` and `below`.
void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry? below, OverlayEntry? above }) {
assert(_debugVerifyInsertPosition(above, below));
assert(
entries.every((OverlayEntry entry) => !_entries.contains(entry)),
'One or more of the specified entries are already present in the Overlay.',
);
assert(
entries.every((OverlayEntry entry) => entry._overlay == null),
'One or more of the specified entries are already present in another Overlay.',
);
if (entries.isEmpty) {
return;
}
for (final OverlayEntry entry in entries) {
assert(entry._overlay == null);
entry._overlay = this;
}
setState(() {
_entries.insertAll(_insertionIndex(below, above), entries);
});
}
bool _debugVerifyInsertPosition(OverlayEntry? above, OverlayEntry? below, { Iterable<OverlayEntry>? newEntries }) {
assert(
above == null || below == null,
'Only one of `above` and `below` may be specified.',
);
assert(
above == null || (above._overlay == this && _entries.contains(above) && (newEntries?.contains(above) ?? true)),
'The provided entry used for `above` must be present in the Overlay${newEntries != null ? ' and in the `newEntriesList`' : ''}.',
);
assert(
below == null || (below._overlay == this && _entries.contains(below) && (newEntries?.contains(below) ?? true)),
'The provided entry used for `below` must be present in the Overlay${newEntries != null ? ' and in the `newEntriesList`' : ''}.',
);
return true;
}
/// Remove all the entries listed in the given iterable, then reinsert them
/// into the overlay in the given order.
///
/// Entries mention in `newEntries` but absent from the overlay are inserted
/// as if with [insertAll].
///
/// Entries not mentioned in `newEntries` but present in the overlay are
/// positioned as a group in the resulting list relative to the entries that
/// were moved, as specified by one of `below` or `above`, which, if
/// specified, must be one of the entries in `newEntries`:
///
/// If `below` is non-null, the group is positioned just below `below`.
/// If `above` is non-null, the group is positioned just above `above`.
/// Otherwise, the group is left on top, with all the rearranged entries
/// below.
///
/// It is an error to specify both `above` and `below`.
void rearrange(Iterable<OverlayEntry> newEntries, { OverlayEntry? below, OverlayEntry? above }) {
final List<OverlayEntry> newEntriesList = newEntries is List<OverlayEntry> ? newEntries : newEntries.toList(growable: false);
assert(_debugVerifyInsertPosition(above, below, newEntries: newEntriesList));
assert(
newEntriesList.every((OverlayEntry entry) => entry._overlay == null || entry._overlay == this),
'One or more of the specified entries are already present in another Overlay.',
);
assert(
newEntriesList.every((OverlayEntry entry) => _entries.indexOf(entry) == _entries.lastIndexOf(entry)),
'One or more of the specified entries are specified multiple times.',
);
if (newEntriesList.isEmpty) {
return;
}
if (listEquals(_entries, newEntriesList)) {
return;
}
final LinkedHashSet<OverlayEntry> old = LinkedHashSet<OverlayEntry>.of(_entries);
for (final OverlayEntry entry in newEntriesList) {
entry._overlay ??= this;
}
setState(() {
_entries.clear();
_entries.addAll(newEntriesList);
old.removeAll(newEntriesList);
_entries.insertAll(_insertionIndex(below, above), old);
});
}
void _markDirty() {
if (mounted) {
setState(() {});
}
}
/// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an
/// opaque entry).
///
/// This is an O(N) algorithm, and should not be necessary except for debug
/// asserts. To avoid people depending on it, this function is implemented
/// only in debug mode, and always returns false in release mode.
bool debugIsVisible(OverlayEntry entry) {
bool result = false;
assert(_entries.contains(entry));
assert(() {
for (int i = _entries.length - 1; i > 0; i -= 1) {
final OverlayEntry candidate = _entries[i];
if (candidate == entry) {
result = true;
break;
}
if (candidate.opaque) {
break;
}
}
return true;
}());
return result;
}
void _didChangeEntryOpacity() {
setState(() {
// We use the opacity of the entry in our build function, which means we
// our state has changed.
});
}
@override
Widget build(BuildContext context) {
// This list is filled backwards and then reversed below before
// it is added to the tree.
final List<_OverlayEntryWidget> children = <_OverlayEntryWidget>[];
bool onstage = true;
int onstageCount = 0;
for (final OverlayEntry entry in _entries.reversed) {
if (onstage) {
onstageCount += 1;
children.add(_OverlayEntryWidget(
key: entry._key,
overlayState: this,
entry: entry,
));
if (entry.opaque) {
onstage = false;
}
} else if (entry.maintainState) {
children.add(_OverlayEntryWidget(
key: entry._key,
overlayState: this,
entry: entry,
tickerEnabled: false,
));
}
}
return _Theater(
skipCount: children.length - onstageCount,
clipBehavior: widget.clipBehavior,
children: children.reversed.toList(growable: false),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
// TODO(jacobr): use IterableProperty instead as that would
// provide a slightly more consistent string summary of the List.
properties.add(DiagnosticsProperty<List<OverlayEntry>>('entries', _entries));
}
}
/// Special version of a [Stack], that doesn't layout and render the first
/// [skipCount] children.
///
/// The first [skipCount] children are considered "offstage".
class _Theater extends MultiChildRenderObjectWidget {
const _Theater({
this.skipCount = 0,
this.clipBehavior = Clip.hardEdge,
required List<_OverlayEntryWidget> super.children,
}) : assert(skipCount >= 0),
assert(children.length >= skipCount);
final int skipCount;
final Clip clipBehavior;
@override
_TheaterElement createElement() => _TheaterElement(this);
@override
_RenderTheater createRenderObject(BuildContext context) {
return _RenderTheater(
skipCount: skipCount,
textDirection: Directionality.of(context),
clipBehavior: clipBehavior,
);
}
@override
void updateRenderObject(BuildContext context, _RenderTheater renderObject) {
renderObject
..skipCount = skipCount
..textDirection = Directionality.of(context)
..clipBehavior = clipBehavior;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('skipCount', skipCount));
}
}
class _TheaterElement extends MultiChildRenderObjectElement {
_TheaterElement(_Theater super.widget);
@override
_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
void debugVisitOnstageChildren(ElementVisitor visitor) {
final _Theater theater = widget as _Theater;
assert(children.length >= theater.skipCount);
children.skip(theater.skipCount).forEach(visitor);
}
}
// A `RenderBox` that sizes itself to its parent's size, implements the stack
// 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,
required TextDirection textDirection,
int skipCount = 0,
Clip clipBehavior = Clip.hardEdge,
}) : assert(skipCount >= 0),
_textDirection = textDirection,
_skipCount = skipCount,
_clipBehavior = clipBehavior {
addAll(children);
}
@override
_RenderTheater get theater => this;
@override
void setupParentData(RenderBox child) {
if (child.parentData is! _TheaterParentData) {
child.parentData = _TheaterParentData();
}
}
@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;
}
}
static void _detachChild(RenderObject child) => child.detach();
@override
void detach() {
super.detach();
RenderBox? child = firstChild;
while (child != null) {
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
childParentData.visitChildrenOfOverlayEntry(_detachChild);
child = childParentData.nextSibling;
}
}
@override
void redepthChildren() => visitChildren(redepthChild);
Alignment? _alignmentCache;
Alignment get _resolvedAlignment => _alignmentCache ??= AlignmentDirectional.topStart.resolve(textDirection);
void _markNeedResolution() {
_alignmentCache = null;
markNeedsLayout();
}
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
if (_textDirection == value) {
return;
}
_textDirection = value;
_markNeedResolution();
}
int get skipCount => _skipCount;
int _skipCount;
set skipCount(int value) {
if (_skipCount != value) {
_skipCount = value;
markNeedsLayout();
}
}
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge], and must not be null.
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.hardEdge;
set clipBehavior(Clip value) {
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
}
// 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 {
if (skipCount == super.childCount) {
return null;
}
RenderBox? child = super.firstChild;
for (int toSkip = skipCount; toSkip > 0; toSkip--) {
final StackParentData childParentData = child!.parentData! as StackParentData;
child = childParentData.nextSibling;
assert(child != null);
}
return child;
}
RenderBox? get _lastOnstageChild => skipCount == super.childCount ? null : lastChild;
@override
double computeMinIntrinsicWidth(double height) {
return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicWidth(height));
}
@override
double computeMaxIntrinsicWidth(double height) {
return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMaxIntrinsicWidth(height));
}
@override
double computeMinIntrinsicHeight(double width) {
return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicHeight(width));
}
@override
double computeMaxIntrinsicHeight(double width) {
return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMaxIntrinsicHeight(width));
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
assert(!debugNeedsLayout);
double? result;
RenderBox? child = _firstOnstageChild;
while (child != null) {
assert(!child.debugNeedsLayout);
final StackParentData childParentData = child.parentData! as StackParentData;
double? candidate = child.getDistanceToActualBaseline(baseline);
if (candidate != null) {
candidate += childParentData.offset.dy;
if (result != null) {
result = math.min(result, candidate);
} else {
result = candidate;
}
}
child = childParentData.nextSibling;
}
return result;
}
@override
Size computeDryLayout(BoxConstraints constraints) {
assert(constraints.biggest.isFinite);
return constraints.biggest;
}
@override
// The following uses sync* because concurrent modifications should be allowed
// during layout.
Iterable<RenderBox> _childrenInPaintOrder() sync* {
RenderBox? child = _firstOnstageChild;
while (child != null) {
yield child;
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
final Iterator<RenderBox>? innerIterator = childParentData.paintOrderIterator;
if (innerIterator != null) {
while (innerIterator.moveNext()) {
yield innerIterator.current;
}
}
child = childParentData.nextSibling;
}
}
@override
// The following uses sync* because hit testing should be lazy.
Iterable<RenderBox> _childrenInHitTestOrder() sync* {
RenderBox? child = _lastOnstageChild;
int childLeft = childCount - skipCount;
while (child != null) {
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
final Iterator<RenderBox>? innerIterator = childParentData.hitTestOrderIterator;
if (innerIterator != null) {
while (innerIterator.moveNext()) {
yield innerIterator.current;
}
}
yield child;
childLeft -= 1;
child = childLeft <= 0 ? null : childParentData.previousSibling;
}
}
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void paint(PaintingContext context, Offset offset) {
if (clipBehavior != Clip.none) {
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
super.paint,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer.layer = null;
super.paint(context, offset);
}
}
@override
void dispose() {
_clipRectLayer.layer = null;
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
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
RenderBox? child = _firstOnstageChild;
while (child != null) {
visitor(child);
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
childParentData.visitChildrenOfOverlayEntry(visitor);
child = childParentData.nextSibling;
}
}
@override
Rect? describeApproximatePaintClip(RenderObject child) {
switch (clipBehavior) {
case Clip.none:
return null;
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
return Offset.zero & size;
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('skipCount', skipCount));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
final List<DiagnosticsNode> offstageChildren = <DiagnosticsNode>[];
final List<DiagnosticsNode> onstageChildren = <DiagnosticsNode>[];
int count = 1;
bool onstage = false;
RenderBox? child = firstChild;
final RenderBox? firstOnstageChild = _firstOnstageChild;
while (child != null) {
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
if (child == firstOnstageChild) {
onstage = true;
count = 1;
}
if (onstage) {
onstageChildren.add(
child.toDiagnosticsNode(
name: 'onstage $count',
),
);
} else {
offstageChildren.add(
child.toDiagnosticsNode(
name: 'offstage $count',
style: DiagnosticsTreeStyle.offstage,
),
);
}
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;
count += 1;
}
return <DiagnosticsNode>[
...onstageChildren,
if (offstageChildren.isNotEmpty)
...offstageChildren
else
DiagnosticsNode.message(
'no offstage children',
style: DiagnosticsTreeStyle.offstage,
),
];
}
}
// * 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.
final 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.
final 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();
}
}