1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'dart:collection'; |
6 | import 'dart:math' as math; |
7 | |
8 | import 'package:flutter/foundation.dart'; |
9 | import 'package:flutter/rendering.dart'; |
10 | import 'package:flutter/scheduler.dart'; |
11 | |
12 | import 'basic.dart'; |
13 | import 'framework.dart'; |
14 | import 'lookup_boundary.dart'; |
15 | import 'ticker_provider.dart'; |
16 | |
17 | const String _flutterWidgetsLibrary = 'package:flutter/widgets.dart' ; |
18 | |
19 | // Examples can assume: |
20 | // late BuildContext context; |
21 | |
22 | // * OverlayEntry Implementation |
23 | |
24 | /// A place in an [Overlay] that can contain a widget. |
25 | /// |
26 | /// Overlay entries are inserted into an [Overlay] using the |
27 | /// [OverlayState.insert] or [OverlayState.insertAll] functions. To find the |
28 | /// closest enclosing overlay for a given [BuildContext], use the [Overlay.of] |
29 | /// function. |
30 | /// |
31 | /// An overlay entry can be in at most one overlay at a time. To remove an entry |
32 | /// from its overlay, call the [remove] function on the overlay entry. |
33 | /// |
34 | /// Because an [Overlay] uses a [Stack] layout, overlay entries can use |
35 | /// [Positioned] and [AnimatedPositioned] to position themselves within the |
36 | /// overlay. |
37 | /// |
38 | /// For example, [Draggable] uses an [OverlayEntry] to show the drag avatar that |
39 | /// follows the user's finger across the screen after the drag begins. Using the |
40 | /// overlay to display the drag avatar lets the avatar float over the other |
41 | /// widgets in the app. As the user's finger moves, draggable calls |
42 | /// [markNeedsBuild] on the overlay entry to cause it to rebuild. In its build, |
43 | /// the entry includes a [Positioned] with its top and left property set to |
44 | /// position the drag avatar near the user's finger. When the drag is over, |
45 | /// [Draggable] removes the entry from the overlay to remove the drag avatar |
46 | /// from view. |
47 | /// |
48 | /// By default, if there is an entirely [opaque] entry over this one, then this |
49 | /// one will not be included in the widget tree (in particular, stateful widgets |
50 | /// within the overlay entry will not be instantiated). To ensure that your |
51 | /// overlay entry is still built even if it is not visible, set [maintainState] |
52 | /// to true. This is more expensive, so should be done with care. In particular, |
53 | /// if widgets in an overlay entry with [maintainState] set to true repeatedly |
54 | /// call [State.setState], the user's battery will be drained unnecessarily. |
55 | /// |
56 | /// [OverlayEntry] is a [Listenable] that notifies when the widget built by |
57 | /// [builder] is mounted or unmounted, whose exact state can be queried by |
58 | /// [mounted]. After the owner of the [OverlayEntry] calls [remove] and then |
59 | /// [dispose], the widget may not be immediately removed from the widget tree. |
60 | /// As a result listeners of the [OverlayEntry] can get notified for one last |
61 | /// time after the [dispose] call, when the widget is eventually unmounted. |
62 | /// |
63 | /// {@macro flutter.widgets.overlayPortalVsOverlayEntry} |
64 | /// |
65 | /// See also: |
66 | /// |
67 | /// * [OverlayPortal], an alternative API for inserting widgets into an |
68 | /// [Overlay] using a builder callback. |
69 | /// * [Overlay], a stack of entries that can be managed independently. |
70 | /// * [OverlayState], the current state of an Overlay. |
71 | /// * [WidgetsApp], a convenience widget that wraps a number of widgets that |
72 | /// are commonly required for an application. |
73 | /// * [MaterialApp], a convenience widget that wraps a number of widgets that |
74 | /// are commonly required for Material Design applications. |
75 | class OverlayEntry implements Listenable { |
76 | /// Creates an overlay entry. |
77 | /// |
78 | /// To insert the entry into an [Overlay], first find the overlay using |
79 | /// [Overlay.of] and then call [OverlayState.insert]. To remove the entry, |
80 | /// call [remove] on the overlay entry itself. |
81 | OverlayEntry({ |
82 | required this.builder, |
83 | bool opaque = false, |
84 | bool maintainState = false, |
85 | this.canSizeOverlay = false, |
86 | }) : _opaque = opaque, |
87 | _maintainState = maintainState { |
88 | if (kFlutterMemoryAllocationsEnabled) { |
89 | _maybeDispatchObjectCreation(); |
90 | } |
91 | } |
92 | |
93 | /// This entry will include the widget built by this builder in the overlay at |
94 | /// the entry's position. |
95 | /// |
96 | /// To cause this builder to be called again, call [markNeedsBuild] on this |
97 | /// overlay entry. |
98 | final WidgetBuilder builder; |
99 | |
100 | /// Whether this entry occludes the entire overlay. |
101 | /// |
102 | /// If an entry claims to be opaque, then, for efficiency, the overlay will |
103 | /// skip building entries below that entry unless they have [maintainState] |
104 | /// set. |
105 | bool get opaque => _opaque; |
106 | bool _opaque; |
107 | set opaque(bool value) { |
108 | assert(!_disposedByOwner); |
109 | if (_opaque == value) { |
110 | return; |
111 | } |
112 | _opaque = value; |
113 | _overlay?._didChangeEntryOpacity(); |
114 | } |
115 | |
116 | /// Whether this entry must be included in the tree even if there is a fully |
117 | /// [opaque] entry above it. |
118 | /// |
119 | /// By default, if there is an entirely [opaque] entry over this one, then this |
120 | /// one will not be included in the widget tree (in particular, stateful widgets |
121 | /// within the overlay entry will not be instantiated). To ensure that your |
122 | /// overlay entry is still built even if it is not visible, set [maintainState] |
123 | /// to true. This is more expensive, so should be done with care. In particular, |
124 | /// if widgets in an overlay entry with [maintainState] set to true repeatedly |
125 | /// call [State.setState], the user's battery will be drained unnecessarily. |
126 | /// |
127 | /// This is used by the [Navigator] and [Route] objects to ensure that routes |
128 | /// are kept around even when in the background, so that [Future]s promised |
129 | /// from subsequent routes will be handled properly when they complete. |
130 | bool get maintainState => _maintainState; |
131 | bool _maintainState; |
132 | set maintainState(bool value) { |
133 | assert(!_disposedByOwner); |
134 | if (_maintainState == value) { |
135 | return; |
136 | } |
137 | _maintainState = value; |
138 | assert(_overlay != null); |
139 | _overlay!._didChangeEntryOpacity(); |
140 | } |
141 | |
142 | /// Whether the content of this [OverlayEntry] can be used to size the |
143 | /// [Overlay]. |
144 | /// |
145 | /// In most situations the overlay sizes itself based on its incoming |
146 | /// constraints to be as large as possible. However, if that would result in |
147 | /// an infinite size, it has to rely on one of its children to size itself. In |
148 | /// this situation, the overlay will consult the topmost non-[Positioned] |
149 | /// overlay entry that has this property set to true, lay it out with the |
150 | /// incoming [BoxConstraints] of the overlay, and force all other |
151 | /// non-[Positioned] overlay entries to have the same size. The [Positioned] |
152 | /// entries are laid out as usual based on the calculated size of the overlay. |
153 | /// |
154 | /// Overlay entries that set this to true must be able to handle unconstrained |
155 | /// [BoxConstraints]. |
156 | /// |
157 | /// Setting this to true has no effect if the overlay entry uses a [Positioned] |
158 | /// widget to position itself in the overlay. |
159 | final bool canSizeOverlay; |
160 | |
161 | /// Whether the [OverlayEntry] is currently mounted in the widget tree. |
162 | /// |
163 | /// The [OverlayEntry] notifies its listeners when this value changes. |
164 | bool get mounted => _overlayEntryStateNotifier?.value != null; |
165 | |
166 | /// The currently mounted `_OverlayEntryWidgetState` built using this [OverlayEntry]. |
167 | ValueNotifier<_OverlayEntryWidgetState?>? _overlayEntryStateNotifier = ValueNotifier<_OverlayEntryWidgetState?>(null); |
168 | |
169 | // TODO(polina-c): stop duplicating code across disposables |
170 | // https://github.com/flutter/flutter/issues/137435 |
171 | /// Dispatches event of object creation to [FlutterMemoryAllocations.instance]. |
172 | void _maybeDispatchObjectCreation() { |
173 | if (kFlutterMemoryAllocationsEnabled) { |
174 | FlutterMemoryAllocations.instance.dispatchObjectCreated( |
175 | library: _flutterWidgetsLibrary, |
176 | className: ' $OverlayEntry' , |
177 | object: this, |
178 | ); |
179 | } |
180 | } |
181 | |
182 | @override |
183 | void addListener(VoidCallback listener) { |
184 | assert(!_disposedByOwner); |
185 | _overlayEntryStateNotifier?.addListener(listener); |
186 | } |
187 | |
188 | @override |
189 | void removeListener(VoidCallback listener) { |
190 | _overlayEntryStateNotifier?.removeListener(listener); |
191 | } |
192 | |
193 | OverlayState? _overlay; |
194 | final GlobalKey<_OverlayEntryWidgetState> _key = GlobalKey<_OverlayEntryWidgetState>(); |
195 | |
196 | /// Remove this entry from the overlay. |
197 | /// |
198 | /// This should only be called once. |
199 | /// |
200 | /// This method removes this overlay entry from the overlay immediately. The |
201 | /// UI will be updated in the same frame if this method is called before the |
202 | /// overlay rebuild in this frame; otherwise, the UI will be updated in the |
203 | /// next frame. This means that it is safe to call during builds, but also |
204 | /// that if you do call this after the overlay rebuild, the UI will not update |
205 | /// until the next frame (i.e. many milliseconds later). |
206 | void remove() { |
207 | assert(_overlay != null); |
208 | assert(!_disposedByOwner); |
209 | final OverlayState overlay = _overlay!; |
210 | _overlay = null; |
211 | if (!overlay.mounted) { |
212 | return; |
213 | } |
214 | |
215 | overlay._entries.remove(this); |
216 | if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { |
217 | SchedulerBinding.instance.addPostFrameCallback((Duration duration) { |
218 | overlay._markDirty(); |
219 | }, debugLabel: 'OverlayEntry.markDirty' ); |
220 | } else { |
221 | overlay._markDirty(); |
222 | } |
223 | } |
224 | |
225 | /// Cause this entry to rebuild during the next pipeline flush. |
226 | /// |
227 | /// You need to call this function if the output of [builder] has changed. |
228 | void markNeedsBuild() { |
229 | assert(!_disposedByOwner); |
230 | _key.currentState?._markNeedsBuild(); |
231 | } |
232 | |
233 | void _didUnmount() { |
234 | assert(!mounted); |
235 | if (_disposedByOwner) { |
236 | _overlayEntryStateNotifier?.dispose(); |
237 | _overlayEntryStateNotifier = null; |
238 | } |
239 | } |
240 | |
241 | bool _disposedByOwner = false; |
242 | |
243 | /// Discards any resources used by this [OverlayEntry]. |
244 | /// |
245 | /// This method must be called after [remove] if the [OverlayEntry] is |
246 | /// inserted into an [Overlay]. |
247 | /// |
248 | /// After this is called, the object is not in a usable state and should be |
249 | /// discarded (calls to [addListener] will throw after the object is disposed). |
250 | /// However, the listeners registered may not be immediately released until |
251 | /// the widget built using this [OverlayEntry] is unmounted from the widget |
252 | /// tree. |
253 | /// |
254 | /// This method should only be called by the object's owner. |
255 | void dispose() { |
256 | assert(!_disposedByOwner); |
257 | assert(_overlay == null, 'An OverlayEntry must first be removed from the Overlay before dispose is called.' ); |
258 | if (kFlutterMemoryAllocationsEnabled) { |
259 | FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this); |
260 | } |
261 | _disposedByOwner = true; |
262 | if (!mounted) { |
263 | // If we're still mounted when disposed, then this will be disposed in |
264 | // _didUnmount, to allow notifications to occur until the entry is |
265 | // unmounted. |
266 | _overlayEntryStateNotifier?.dispose(); |
267 | _overlayEntryStateNotifier = null; |
268 | } |
269 | } |
270 | |
271 | @override |
272 | String toString() => ' ${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState) ${_disposedByOwner ? "(DISPOSED)" : "" }' ; |
273 | } |
274 | |
275 | class _OverlayEntryWidget extends StatefulWidget { |
276 | const _OverlayEntryWidget({ |
277 | required Key key, |
278 | required this.entry, |
279 | required this.overlayState, |
280 | this.tickerEnabled = true, |
281 | }) : super(key: key); |
282 | |
283 | final OverlayEntry entry; |
284 | final OverlayState overlayState; |
285 | final bool tickerEnabled; |
286 | |
287 | @override |
288 | _OverlayEntryWidgetState createState() => _OverlayEntryWidgetState(); |
289 | } |
290 | |
291 | class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> { |
292 | late _RenderTheater _theater; |
293 | |
294 | // Manages the stack of theater children whose paint order are sorted by their |
295 | // _zOrderIndex. The children added by OverlayPortal are added to this linked |
296 | // list, and they will be shown _above_ the OverlayEntry tied to this widget. |
297 | // The children with larger zOrderIndex values (i.e. those called `show` |
298 | // recently) will be painted last. |
299 | // |
300 | // This linked list is lazily created in `_add`, and the entries are added/removed |
301 | // via `_add`/`_remove`, called by OverlayPortals lower in the tree. `_add` or |
302 | // `_remove` does not cause this widget to rebuild, the linked list will be |
303 | // read by _RenderTheater as part of its render child model. This would ideally |
304 | // be in a RenderObject but there may not be RenderObjects between |
305 | // _RenderTheater and the render subtree OverlayEntry builds. |
306 | LinkedList<_OverlayEntryLocation>? _sortedTheaterSiblings; |
307 | |
308 | // Worst-case O(N), N being the number of children added to the top spot in |
309 | // the same frame. This can be a bit expensive when there's a lot of global |
310 | // key reparenting in the same frame but N is usually a small number. |
311 | void _add(_OverlayEntryLocation child) { |
312 | assert(mounted); |
313 | final LinkedList<_OverlayEntryLocation> children = _sortedTheaterSiblings ??= LinkedList<_OverlayEntryLocation>(); |
314 | assert(!children.contains(child)); |
315 | _OverlayEntryLocation? insertPosition = children.isEmpty ? null : children.last; |
316 | while (insertPosition != null && insertPosition._zOrderIndex > child._zOrderIndex) { |
317 | insertPosition = insertPosition.previous; |
318 | } |
319 | if (insertPosition == null) { |
320 | children.addFirst(child); |
321 | } else { |
322 | insertPosition.insertAfter(child); |
323 | } |
324 | assert(children.contains(child)); |
325 | } |
326 | |
327 | void _remove(_OverlayEntryLocation child) { |
328 | assert(_sortedTheaterSiblings != null); |
329 | final bool wasInCollection = _sortedTheaterSiblings?.remove(child) ?? false; |
330 | assert(wasInCollection); |
331 | } |
332 | |
333 | // Returns an Iterable that traverse the children in the child model in paint |
334 | // order (from farthest to the user to the closest to the user). |
335 | // |
336 | // The iterator should be safe to use even when the child model is being |
337 | // mutated. The reason for that is it's allowed to add/remove/move deferred |
338 | // children to a _RenderTheater during performLayout, but the affected |
339 | // children don't have to be laid out in the same performLayout call. |
340 | late final Iterable<RenderBox> _paintOrderIterable = _createChildIterable(reversed: false); |
341 | // An Iterable that traverse the children in the child model in |
342 | // hit-test order (from closest to the user to the farthest to the user). |
343 | late final Iterable<RenderBox> _hitTestOrderIterable = _createChildIterable(reversed: true); |
344 | |
345 | // The following uses sync* because hit-testing is lazy, and LinkedList as a |
346 | // Iterable doesn't support concurrent modification. |
347 | Iterable<RenderBox> _createChildIterable({ required bool reversed }) sync* { |
348 | final LinkedList<_OverlayEntryLocation>? children = _sortedTheaterSiblings; |
349 | if (children == null || children.isEmpty) { |
350 | return; |
351 | } |
352 | _OverlayEntryLocation? candidate = reversed ? children.last : children.first; |
353 | while (candidate != null) { |
354 | final RenderBox? renderBox = candidate._overlayChildRenderBox; |
355 | candidate = reversed ? candidate.previous : candidate.next; |
356 | if (renderBox != null) { |
357 | yield renderBox; |
358 | } |
359 | } |
360 | } |
361 | |
362 | @override |
363 | void initState() { |
364 | super.initState(); |
365 | widget.entry._overlayEntryStateNotifier!.value = this; |
366 | _theater = context.findAncestorRenderObjectOfType<_RenderTheater>()!; |
367 | assert(_sortedTheaterSiblings == null); |
368 | } |
369 | |
370 | @override |
371 | void didUpdateWidget(_OverlayEntryWidget oldWidget) { |
372 | super.didUpdateWidget(oldWidget); |
373 | // OverlayState's build method always returns a RenderObjectWidget _Theater, |
374 | // so it's safe to assume that state equality implies render object equality. |
375 | assert(oldWidget.entry == widget.entry); |
376 | if (oldWidget.overlayState != widget.overlayState) { |
377 | final _RenderTheater newTheater = context.findAncestorRenderObjectOfType<_RenderTheater>()!; |
378 | assert(_theater != newTheater); |
379 | _theater = newTheater; |
380 | } |
381 | } |
382 | |
383 | @override |
384 | void dispose() { |
385 | widget.entry._overlayEntryStateNotifier?.value = null; |
386 | widget.entry._didUnmount(); |
387 | _sortedTheaterSiblings = null; |
388 | super.dispose(); |
389 | } |
390 | |
391 | @override |
392 | Widget build(BuildContext context) { |
393 | return TickerMode( |
394 | enabled: widget.tickerEnabled, |
395 | child: _RenderTheaterMarker( |
396 | theater: _theater, |
397 | overlayEntryWidgetState: this, |
398 | child: widget.entry.builder(context), |
399 | ), |
400 | ); |
401 | } |
402 | |
403 | void _markNeedsBuild() { |
404 | setState(() { /* the state that changed is in the builder */ }); |
405 | } |
406 | } |
407 | |
408 | /// A stack of entries that can be managed independently. |
409 | /// |
410 | /// Overlays let independent child widgets "float" visual elements on top of |
411 | /// other widgets by inserting them into the overlay's stack. The overlay lets |
412 | /// each of these widgets manage their participation in the overlay using |
413 | /// [OverlayEntry] objects. |
414 | /// |
415 | /// Although you can create an [Overlay] directly, it's most common to use the |
416 | /// overlay created by the [Navigator] in a [WidgetsApp], [CupertinoApp] or a |
417 | /// [MaterialApp]. The navigator uses its overlay to manage the visual |
418 | /// appearance of its routes. |
419 | /// |
420 | /// The [Overlay] widget uses a custom stack implementation, which is very |
421 | /// similar to the [Stack] widget. The main use case of [Overlay] is related to |
422 | /// navigation and being able to insert widgets on top of the pages in an app. |
423 | /// For layout purposes unrelated to navigation, consider using [Stack] instead. |
424 | /// |
425 | /// An [Overlay] widget requires a [Directionality] widget to be in scope, so |
426 | /// that it can resolve direction-sensitive coordinates of any |
427 | /// [Positioned.directional] children. |
428 | /// |
429 | /// For widgets drawn in an [OverlayEntry], do not assume that the size of the |
430 | /// [Overlay] is the size returned by [MediaQuery.sizeOf]. Nested overlays can |
431 | /// have different sizes. |
432 | /// |
433 | /// {@tool dartpad} |
434 | /// This example shows how to use the [Overlay] to highlight the [NavigationBar] |
435 | /// destination. |
436 | /// |
437 | /// ** See code in examples/api/lib/widgets/overlay/overlay.0.dart ** |
438 | /// {@end-tool} |
439 | /// |
440 | /// See also: |
441 | /// |
442 | /// * [OverlayEntry], the class that is used for describing the overlay entries. |
443 | /// * [OverlayState], which is used to insert the entries into the overlay. |
444 | /// * [WidgetsApp], which inserts an [Overlay] widget indirectly via its [Navigator]. |
445 | /// * [MaterialApp], which inserts an [Overlay] widget indirectly via its [Navigator]. |
446 | /// * [CupertinoApp], which inserts an [Overlay] widget indirectly via its [Navigator]. |
447 | /// * [Stack], which allows directly displaying a stack of widgets. |
448 | class Overlay extends StatefulWidget { |
449 | /// Creates an overlay. |
450 | /// |
451 | /// The initial entries will be inserted into the overlay when its associated |
452 | /// [OverlayState] is initialized. |
453 | /// |
454 | /// Rather than creating an overlay, consider using the overlay that is |
455 | /// created by the [Navigator] in a [WidgetsApp], [CupertinoApp], or a |
456 | /// [MaterialApp] for the application. |
457 | const Overlay({ |
458 | super.key, |
459 | this.initialEntries = const <OverlayEntry>[], |
460 | this.clipBehavior = Clip.hardEdge, |
461 | }); |
462 | |
463 | /// Wrap the provided `child` in an [Overlay] to allow other visual elements |
464 | /// (packed in [OverlayEntry]s) to float on top of the child. |
465 | /// |
466 | /// This is a convenience method over the regular [Overlay] constructor: It |
467 | /// creates an [Overlay] and puts the provided `child` in an [OverlayEntry] |
468 | /// at the bottom of that newly created Overlay. |
469 | static Widget wrap({ |
470 | Key? key, |
471 | Clip clipBehavior = Clip.hardEdge, |
472 | required Widget child, |
473 | }) { |
474 | return _WrappingOverlay(key: key, clipBehavior: clipBehavior, child: child); |
475 | } |
476 | |
477 | /// The entries to include in the overlay initially. |
478 | /// |
479 | /// These entries are only used when the [OverlayState] is initialized. If you |
480 | /// are providing a new [Overlay] description for an overlay that's already in |
481 | /// the tree, then the new entries are ignored. |
482 | /// |
483 | /// To add entries to an [Overlay] that is already in the tree, use |
484 | /// [Overlay.of] to obtain the [OverlayState] (or assign a [GlobalKey] to the |
485 | /// [Overlay] widget and obtain the [OverlayState] via |
486 | /// [GlobalKey.currentState]), and then use [OverlayState.insert] or |
487 | /// [OverlayState.insertAll]. |
488 | /// |
489 | /// To remove an entry from an [Overlay], use [OverlayEntry.remove]. |
490 | final List<OverlayEntry> initialEntries; |
491 | |
492 | /// {@macro flutter.material.Material.clipBehavior} |
493 | /// |
494 | /// Defaults to [Clip.hardEdge]. |
495 | final Clip clipBehavior; |
496 | |
497 | /// The [OverlayState] from the closest instance of [Overlay] that encloses |
498 | /// the given context within the closest [LookupBoundary], and, in debug mode, |
499 | /// will throw if one is not found. |
500 | /// |
501 | /// In debug mode, if the `debugRequiredFor` argument is provided and an |
502 | /// overlay isn't found, then this function will throw an exception containing |
503 | /// the runtime type of the given widget in the error message. The exception |
504 | /// attempts to explain that the calling [Widget] (the one given by the |
505 | /// `debugRequiredFor` argument) needs an [Overlay] to be present to function. |
506 | /// If `debugRequiredFor` is not supplied, then the error message is more |
507 | /// generic. |
508 | /// |
509 | /// Typical usage is as follows: |
510 | /// |
511 | /// ```dart |
512 | /// OverlayState overlay = Overlay.of(context); |
513 | /// ``` |
514 | /// |
515 | /// If `rootOverlay` is set to true, the state from the furthest instance of |
516 | /// this class is given instead. Useful for installing overlay entries above |
517 | /// all subsequent instances of [Overlay]. |
518 | /// |
519 | /// This method can be expensive (it walks the element tree). |
520 | /// |
521 | /// See also: |
522 | /// |
523 | /// * [Overlay.maybeOf] for a similar function that returns null if an |
524 | /// [Overlay] is not found. |
525 | static OverlayState of( |
526 | BuildContext context, { |
527 | bool rootOverlay = false, |
528 | Widget? debugRequiredFor, |
529 | }) { |
530 | final OverlayState? result = maybeOf(context, rootOverlay: rootOverlay); |
531 | assert(() { |
532 | if (result == null) { |
533 | final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorStateOfType<OverlayState>(context); |
534 | final List<DiagnosticsNode> information = <DiagnosticsNode>[ |
535 | ErrorSummary('No Overlay widget found ${hiddenByBoundary ? ' within the closest LookupBoundary' : '' }.' ), |
536 | if (hiddenByBoundary) |
537 | ErrorDescription( |
538 | 'There is an ancestor Overlay widget, but it is hidden by a LookupBoundary.' |
539 | ), |
540 | ErrorDescription(' ${debugRequiredFor?.runtimeType ?? 'Some' } widgets require an Overlay widget ancestor for correct operation.' ), |
541 | 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.' ), |
542 | if (debugRequiredFor != null) DiagnosticsProperty<Widget>('The specific widget that failed to find an overlay was' , debugRequiredFor, style: DiagnosticsTreeStyle.errorProperty), |
543 | if (context.widget != debugRequiredFor) |
544 | context.describeElement('The context from which that widget was searching for an overlay was' ), |
545 | ]; |
546 | |
547 | throw FlutterError.fromParts(information); |
548 | } |
549 | return true; |
550 | }()); |
551 | return result!; |
552 | } |
553 | |
554 | /// The [OverlayState] from the closest instance of [Overlay] that encloses |
555 | /// the given context within the closest [LookupBoundary], if any. |
556 | /// |
557 | /// Typical usage is as follows: |
558 | /// |
559 | /// ```dart |
560 | /// OverlayState? overlay = Overlay.maybeOf(context); |
561 | /// ``` |
562 | /// |
563 | /// If `rootOverlay` is set to true, the state from the furthest instance of |
564 | /// this class is given instead. Useful for installing overlay entries above |
565 | /// all subsequent instances of [Overlay]. |
566 | /// |
567 | /// This method can be expensive (it walks the element tree). |
568 | /// |
569 | /// See also: |
570 | /// |
571 | /// * [Overlay.of] for a similar function that returns a non-nullable result |
572 | /// and throws if an [Overlay] is not found. |
573 | |
574 | static OverlayState? maybeOf( |
575 | BuildContext context, { |
576 | bool rootOverlay = false, |
577 | }) { |
578 | return rootOverlay |
579 | ? LookupBoundary.findRootAncestorStateOfType<OverlayState>(context) |
580 | : LookupBoundary.findAncestorStateOfType<OverlayState>(context); |
581 | } |
582 | |
583 | @override |
584 | OverlayState createState() => OverlayState(); |
585 | } |
586 | |
587 | /// The current state of an [Overlay]. |
588 | /// |
589 | /// Used to insert [OverlayEntry]s into the overlay using the [insert] and |
590 | /// [insertAll] functions. |
591 | class OverlayState extends State<Overlay> with TickerProviderStateMixin { |
592 | final List<OverlayEntry> _entries = <OverlayEntry>[]; |
593 | |
594 | @override |
595 | void initState() { |
596 | super.initState(); |
597 | insertAll(widget.initialEntries); |
598 | } |
599 | |
600 | int _insertionIndex(OverlayEntry? below, OverlayEntry? above) { |
601 | assert(above == null || below == null); |
602 | if (below != null) { |
603 | return _entries.indexOf(below); |
604 | } |
605 | if (above != null) { |
606 | return _entries.indexOf(above) + 1; |
607 | } |
608 | return _entries.length; |
609 | } |
610 | |
611 | bool _debugCanInsertEntry(OverlayEntry entry) { |
612 | final List<DiagnosticsNode> operandsInformation = <DiagnosticsNode>[ |
613 | DiagnosticsProperty<OverlayEntry>('The OverlayEntry was' , entry, style: DiagnosticsTreeStyle.errorProperty), |
614 | DiagnosticsProperty<OverlayState>( |
615 | 'The Overlay the OverlayEntry was trying to insert to was' , this, style: DiagnosticsTreeStyle.errorProperty, |
616 | ), |
617 | ]; |
618 | |
619 | if (!mounted) { |
620 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
621 | ErrorSummary('Attempted to insert an OverlayEntry to an already disposed Overlay.' ), |
622 | ...operandsInformation, |
623 | ]); |
624 | } |
625 | |
626 | final OverlayState? currentOverlay = entry._overlay; |
627 | final bool alreadyContainsEntry = _entries.contains(entry); |
628 | |
629 | if (alreadyContainsEntry) { |
630 | final bool inconsistentOverlayState = !identical(currentOverlay, this); |
631 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
632 | ErrorSummary('The specified entry is already present in the target Overlay.' ), |
633 | ...operandsInformation, |
634 | if (inconsistentOverlayState) ErrorHint('This could be an error in the Flutter framework.' ) |
635 | else ErrorHint( |
636 | 'Consider calling remove on the OverlayEntry before inserting it to a different Overlay, ' |
637 | 'or switching to the OverlayPortal API to avoid manual OverlayEntry management.' |
638 | ), |
639 | if (inconsistentOverlayState) DiagnosticsProperty<OverlayState>( |
640 | "The OverlayEntry's current Overlay was" , currentOverlay, style: DiagnosticsTreeStyle.errorProperty, |
641 | ), |
642 | ]); |
643 | } |
644 | |
645 | if (currentOverlay == null) { |
646 | return true; |
647 | } |
648 | |
649 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
650 | ErrorSummary('The specified entry is already present in a different Overlay.' ), |
651 | ...operandsInformation, |
652 | DiagnosticsProperty<OverlayState>("The OverlayEntry's current Overlay was" , currentOverlay, style: DiagnosticsTreeStyle.errorProperty,), |
653 | ErrorHint( |
654 | 'Consider calling remove on the OverlayEntry before inserting it to a different Overlay, ' |
655 | 'or switching to the OverlayPortal API to avoid manual OverlayEntry management.' |
656 | ) |
657 | ]); |
658 | } |
659 | |
660 | /// Insert the given entry into the overlay. |
661 | /// |
662 | /// If `below` is non-null, the entry is inserted just below `below`. |
663 | /// If `above` is non-null, the entry is inserted just above `above`. |
664 | /// Otherwise, the entry is inserted on top. |
665 | /// |
666 | /// It is an error to specify both `above` and `below`. |
667 | void insert(OverlayEntry entry, { OverlayEntry? below, OverlayEntry? above }) { |
668 | assert(_debugVerifyInsertPosition(above, below)); |
669 | assert(_debugCanInsertEntry(entry)); |
670 | entry._overlay = this; |
671 | setState(() { |
672 | _entries.insert(_insertionIndex(below, above), entry); |
673 | }); |
674 | } |
675 | |
676 | /// Insert all the entries in the given iterable. |
677 | /// |
678 | /// If `below` is non-null, the entries are inserted just below `below`. |
679 | /// If `above` is non-null, the entries are inserted just above `above`. |
680 | /// Otherwise, the entries are inserted on top. |
681 | /// |
682 | /// It is an error to specify both `above` and `below`. |
683 | void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry? below, OverlayEntry? above }) { |
684 | assert(_debugVerifyInsertPosition(above, below)); |
685 | assert(entries.every(_debugCanInsertEntry)); |
686 | if (entries.isEmpty) { |
687 | return; |
688 | } |
689 | for (final OverlayEntry entry in entries) { |
690 | assert(entry._overlay == null); |
691 | entry._overlay = this; |
692 | } |
693 | setState(() { |
694 | _entries.insertAll(_insertionIndex(below, above), entries); |
695 | }); |
696 | } |
697 | |
698 | bool _debugVerifyInsertPosition(OverlayEntry? above, OverlayEntry? below, { Iterable<OverlayEntry>? newEntries }) { |
699 | assert( |
700 | above == null || below == null, |
701 | 'Only one of `above` and `below` may be specified.' , |
702 | ); |
703 | assert( |
704 | above == null || (above._overlay == this && _entries.contains(above) && (newEntries?.contains(above) ?? true)), |
705 | 'The provided entry used for `above` must be present in the Overlay ${newEntries != null ? ' and in the `newEntriesList`' : '' }.' , |
706 | ); |
707 | assert( |
708 | below == null || (below._overlay == this && _entries.contains(below) && (newEntries?.contains(below) ?? true)), |
709 | 'The provided entry used for `below` must be present in the Overlay ${newEntries != null ? ' and in the `newEntriesList`' : '' }.' , |
710 | ); |
711 | return true; |
712 | } |
713 | |
714 | /// Remove all the entries listed in the given iterable, then reinsert them |
715 | /// into the overlay in the given order. |
716 | /// |
717 | /// Entries mention in `newEntries` but absent from the overlay are inserted |
718 | /// as if with [insertAll]. |
719 | /// |
720 | /// Entries not mentioned in `newEntries` but present in the overlay are |
721 | /// positioned as a group in the resulting list relative to the entries that |
722 | /// were moved, as specified by one of `below` or `above`, which, if |
723 | /// specified, must be one of the entries in `newEntries`: |
724 | /// |
725 | /// If `below` is non-null, the group is positioned just below `below`. |
726 | /// If `above` is non-null, the group is positioned just above `above`. |
727 | /// Otherwise, the group is left on top, with all the rearranged entries |
728 | /// below. |
729 | /// |
730 | /// It is an error to specify both `above` and `below`. |
731 | void rearrange(Iterable<OverlayEntry> newEntries, { OverlayEntry? below, OverlayEntry? above }) { |
732 | final List<OverlayEntry> newEntriesList = newEntries is List<OverlayEntry> ? newEntries : newEntries.toList(growable: false); |
733 | assert(_debugVerifyInsertPosition(above, below, newEntries: newEntriesList)); |
734 | assert( |
735 | newEntriesList.every((OverlayEntry entry) => entry._overlay == null || entry._overlay == this), |
736 | 'One or more of the specified entries are already present in another Overlay.' , |
737 | ); |
738 | assert( |
739 | newEntriesList.every((OverlayEntry entry) => _entries.indexOf(entry) == _entries.lastIndexOf(entry)), |
740 | 'One or more of the specified entries are specified multiple times.' , |
741 | ); |
742 | if (newEntriesList.isEmpty) { |
743 | return; |
744 | } |
745 | if (listEquals(_entries, newEntriesList)) { |
746 | return; |
747 | } |
748 | final LinkedHashSet<OverlayEntry> old = LinkedHashSet<OverlayEntry>.of(_entries); |
749 | for (final OverlayEntry entry in newEntriesList) { |
750 | entry._overlay ??= this; |
751 | } |
752 | setState(() { |
753 | _entries.clear(); |
754 | _entries.addAll(newEntriesList); |
755 | old.removeAll(newEntriesList); |
756 | _entries.insertAll(_insertionIndex(below, above), old); |
757 | }); |
758 | } |
759 | |
760 | void _markDirty() { |
761 | if (mounted) { |
762 | setState(() {}); |
763 | } |
764 | } |
765 | |
766 | /// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an |
767 | /// opaque entry). |
768 | /// |
769 | /// This is an O(N) algorithm, and should not be necessary except for debug |
770 | /// asserts. To avoid people depending on it, this function is implemented |
771 | /// only in debug mode, and always returns false in release mode. |
772 | bool debugIsVisible(OverlayEntry entry) { |
773 | bool result = false; |
774 | assert(_entries.contains(entry)); |
775 | assert(() { |
776 | for (int i = _entries.length - 1; i > 0; i -= 1) { |
777 | final OverlayEntry candidate = _entries[i]; |
778 | if (candidate == entry) { |
779 | result = true; |
780 | break; |
781 | } |
782 | if (candidate.opaque) { |
783 | break; |
784 | } |
785 | } |
786 | return true; |
787 | }()); |
788 | return result; |
789 | } |
790 | |
791 | void _didChangeEntryOpacity() { |
792 | setState(() { |
793 | // We use the opacity of the entry in our build function, which means we |
794 | // our state has changed. |
795 | }); |
796 | } |
797 | |
798 | @override |
799 | Widget build(BuildContext context) { |
800 | // This list is filled backwards and then reversed below before |
801 | // it is added to the tree. |
802 | final List<_OverlayEntryWidget> children = <_OverlayEntryWidget>[]; |
803 | bool onstage = true; |
804 | int onstageCount = 0; |
805 | for (final OverlayEntry entry in _entries.reversed) { |
806 | if (onstage) { |
807 | onstageCount += 1; |
808 | children.add(_OverlayEntryWidget( |
809 | key: entry._key, |
810 | overlayState: this, |
811 | entry: entry, |
812 | )); |
813 | if (entry.opaque) { |
814 | onstage = false; |
815 | } |
816 | } else if (entry.maintainState) { |
817 | children.add(_OverlayEntryWidget( |
818 | key: entry._key, |
819 | overlayState: this, |
820 | entry: entry, |
821 | tickerEnabled: false, |
822 | )); |
823 | } |
824 | } |
825 | return _Theater( |
826 | skipCount: children.length - onstageCount, |
827 | clipBehavior: widget.clipBehavior, |
828 | children: children.reversed.toList(growable: false), |
829 | ); |
830 | } |
831 | |
832 | @override |
833 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
834 | super.debugFillProperties(properties); |
835 | // TODO(jacobr): use IterableProperty instead as that would |
836 | // provide a slightly more consistent string summary of the List. |
837 | properties.add(DiagnosticsProperty<List<OverlayEntry>>('entries' , _entries)); |
838 | } |
839 | } |
840 | |
841 | class _WrappingOverlay extends StatefulWidget { |
842 | const _WrappingOverlay({super.key, this.clipBehavior = Clip.hardEdge, required this.child}); |
843 | |
844 | final Clip clipBehavior; |
845 | final Widget child; |
846 | |
847 | @override |
848 | State<_WrappingOverlay> createState() => _WrappingOverlayState(); |
849 | } |
850 | |
851 | class _WrappingOverlayState extends State<_WrappingOverlay> { |
852 | late final OverlayEntry _entry = OverlayEntry( |
853 | canSizeOverlay: true, |
854 | opaque: true, |
855 | builder: (BuildContext context) { |
856 | return widget.child; |
857 | } |
858 | ); |
859 | |
860 | @override |
861 | void didUpdateWidget(_WrappingOverlay oldWidget) { |
862 | super.didUpdateWidget(oldWidget); |
863 | _entry.markNeedsBuild(); |
864 | } |
865 | |
866 | @override |
867 | Widget build(BuildContext context) { |
868 | return Overlay( |
869 | clipBehavior: widget.clipBehavior, |
870 | initialEntries: <OverlayEntry>[_entry], |
871 | ); |
872 | } |
873 | } |
874 | |
875 | /// Special version of a [Stack], that doesn't layout and render the first |
876 | /// [skipCount] children. |
877 | /// |
878 | /// The first [skipCount] children are considered "offstage". |
879 | class _Theater extends MultiChildRenderObjectWidget { |
880 | const _Theater({ |
881 | this.skipCount = 0, |
882 | this.clipBehavior = Clip.hardEdge, |
883 | required List<_OverlayEntryWidget> super.children, |
884 | }) : assert(skipCount >= 0), |
885 | assert(children.length >= skipCount); |
886 | |
887 | final int skipCount; |
888 | |
889 | final Clip clipBehavior; |
890 | |
891 | @override |
892 | _TheaterElement createElement() => _TheaterElement(this); |
893 | |
894 | @override |
895 | _RenderTheater createRenderObject(BuildContext context) { |
896 | return _RenderTheater( |
897 | skipCount: skipCount, |
898 | textDirection: Directionality.of(context), |
899 | clipBehavior: clipBehavior, |
900 | ); |
901 | } |
902 | |
903 | @override |
904 | void updateRenderObject(BuildContext context, _RenderTheater renderObject) { |
905 | renderObject |
906 | ..skipCount = skipCount |
907 | ..textDirection = Directionality.of(context) |
908 | ..clipBehavior = clipBehavior; |
909 | } |
910 | |
911 | @override |
912 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
913 | super.debugFillProperties(properties); |
914 | properties.add(IntProperty('skipCount' , skipCount)); |
915 | } |
916 | } |
917 | |
918 | class _TheaterElement extends MultiChildRenderObjectElement { |
919 | _TheaterElement(_Theater super.widget); |
920 | |
921 | @override |
922 | _RenderTheater get renderObject => super.renderObject as _RenderTheater; |
923 | |
924 | @override |
925 | void insertRenderObjectChild(RenderBox child, IndexedSlot<Element?> slot) { |
926 | super.insertRenderObjectChild(child, slot); |
927 | final _TheaterParentData parentData = child.parentData! as _TheaterParentData; |
928 | parentData.overlayEntry = ((widget as _Theater).children[slot.index] as _OverlayEntryWidget).entry; |
929 | assert(parentData.overlayEntry != null); |
930 | } |
931 | |
932 | @override |
933 | void moveRenderObjectChild(RenderBox child, IndexedSlot<Element?> oldSlot, IndexedSlot<Element?> newSlot) { |
934 | super.moveRenderObjectChild(child, oldSlot, newSlot); |
935 | assert(() { |
936 | final _TheaterParentData parentData = child.parentData! as _TheaterParentData; |
937 | return parentData.overlayEntry == ((widget as _Theater).children[newSlot.index] as _OverlayEntryWidget).entry; |
938 | }()); |
939 | } |
940 | |
941 | @override |
942 | void debugVisitOnstageChildren(ElementVisitor visitor) { |
943 | final _Theater theater = widget as _Theater; |
944 | assert(children.length >= theater.skipCount); |
945 | children.skip(theater.skipCount).forEach(visitor); |
946 | } |
947 | } |
948 | |
949 | // A `RenderBox` that sizes itself to its parent's size, implements the stack |
950 | // layout algorithm and renders its children in the given `theater`. |
951 | mixin _RenderTheaterMixin on RenderBox { |
952 | _RenderTheater get theater; |
953 | |
954 | Iterable<RenderBox> _childrenInPaintOrder(); |
955 | Iterable<RenderBox> _childrenInHitTestOrder(); |
956 | |
957 | @override |
958 | void setupParentData(RenderBox child) { |
959 | if (child.parentData is! StackParentData) { |
960 | child.parentData = StackParentData(); |
961 | } |
962 | } |
963 | |
964 | void layoutChild(RenderBox child, BoxConstraints nonPositionedChildConstraints) { |
965 | final StackParentData childParentData = child.parentData! as StackParentData; |
966 | final Alignment alignment = theater._resolvedAlignment; |
967 | if (!childParentData.isPositioned) { |
968 | child.layout(nonPositionedChildConstraints, parentUsesSize: true); |
969 | childParentData.offset = Offset.zero; |
970 | } else { |
971 | assert(child is! _RenderDeferredLayoutBox, 'all _RenderDeferredLayoutBoxes must be non-positioned children.' ); |
972 | RenderStack.layoutPositionedChild(child, childParentData, size, alignment); |
973 | } |
974 | assert(child.parentData == childParentData); |
975 | } |
976 | |
977 | @override |
978 | bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |
979 | final Iterator<RenderBox> iterator = _childrenInHitTestOrder().iterator; |
980 | bool isHit = false; |
981 | while (!isHit && iterator.moveNext()) { |
982 | final RenderBox child = iterator.current; |
983 | final StackParentData childParentData = child.parentData! as StackParentData; |
984 | final RenderBox localChild = child; |
985 | bool childHitTest(BoxHitTestResult result, Offset position) => localChild.hitTest(result, position: position); |
986 | isHit = result.addWithPaintOffset(offset: childParentData.offset, position: position, hitTest: childHitTest); |
987 | } |
988 | return isHit; |
989 | } |
990 | |
991 | @override |
992 | void paint(PaintingContext context, Offset offset) { |
993 | for (final RenderBox child in _childrenInPaintOrder()) { |
994 | final StackParentData childParentData = child.parentData! as StackParentData; |
995 | context.paintChild(child, childParentData.offset + offset); |
996 | } |
997 | } |
998 | } |
999 | |
1000 | class _TheaterParentData extends StackParentData { |
1001 | // The OverlayEntry that directly created this child. This field is null for |
1002 | // children that are created by an OverlayPortal. |
1003 | OverlayEntry? overlayEntry; |
1004 | |
1005 | /// A [OverlayPortal] makes its overlay child a render child of an ancestor |
1006 | /// [Overlay]. Currently, to make sure the overlay child is painted after its |
1007 | /// [OverlayPortal], and before the next [OverlayEntry] (which could be |
1008 | /// something that should obstruct the overlay child, such as a [ModalRoute]) |
1009 | /// in the host [Overlay], the paint order of each overlay child is managed by |
1010 | /// the [OverlayEntry] that hosts its [OverlayPortal]. |
1011 | /// |
1012 | /// The following methods are exposed to allow easy access to the overlay |
1013 | /// children's render objects whose order is managed by [overlayEntry], in the |
1014 | /// right order. |
1015 | |
1016 | // _overlayStateMounted is set to null in _OverlayEntryWidgetState's dispose |
1017 | // method. This property is only accessed during layout, paint and hit-test so |
1018 | // the `value!` should be safe. |
1019 | Iterator<RenderBox>? get paintOrderIterator => overlayEntry?._overlayEntryStateNotifier?.value!._paintOrderIterable.iterator; |
1020 | Iterator<RenderBox>? get hitTestOrderIterator => overlayEntry?._overlayEntryStateNotifier?.value!._hitTestOrderIterable.iterator; |
1021 | |
1022 | // A convenience method for traversing `paintOrderIterator` with a |
1023 | // [RenderObjectVisitor]. |
1024 | void visitOverlayPortalChildrenOnOverlayEntry(RenderObjectVisitor visitor) => overlayEntry?._overlayEntryStateNotifier?.value!._paintOrderIterable.forEach(visitor); |
1025 | } |
1026 | |
1027 | class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox, StackParentData>, _RenderTheaterMixin { |
1028 | _RenderTheater({ |
1029 | List<RenderBox>? children, |
1030 | required TextDirection textDirection, |
1031 | int skipCount = 0, |
1032 | Clip clipBehavior = Clip.hardEdge, |
1033 | }) : assert(skipCount >= 0), |
1034 | _textDirection = textDirection, |
1035 | _skipCount = skipCount, |
1036 | _clipBehavior = clipBehavior { |
1037 | addAll(children); |
1038 | } |
1039 | |
1040 | @override |
1041 | _RenderTheater get theater => this; |
1042 | |
1043 | @override |
1044 | void setupParentData(RenderBox child) { |
1045 | if (child.parentData is! _TheaterParentData) { |
1046 | child.parentData = _TheaterParentData(); |
1047 | } |
1048 | } |
1049 | |
1050 | @override |
1051 | void attach(PipelineOwner owner) { |
1052 | super.attach(owner); |
1053 | RenderBox? child = firstChild; |
1054 | while (child != null) { |
1055 | final _TheaterParentData childParentData = child.parentData! as _TheaterParentData; |
1056 | final Iterator<RenderBox>? iterator = childParentData.paintOrderIterator; |
1057 | if (iterator != null) { |
1058 | while (iterator.moveNext()) { |
1059 | iterator.current.attach(owner); |
1060 | } |
1061 | } |
1062 | child = childParentData.nextSibling; |
1063 | } |
1064 | } |
1065 | |
1066 | static void _detachChild(RenderObject child) => child.detach(); |
1067 | |
1068 | @override |
1069 | void detach() { |
1070 | super.detach(); |
1071 | RenderBox? child = firstChild; |
1072 | while (child != null) { |
1073 | final _TheaterParentData childParentData = child.parentData! as _TheaterParentData; |
1074 | childParentData.visitOverlayPortalChildrenOnOverlayEntry(_detachChild); |
1075 | child = childParentData.nextSibling; |
1076 | } |
1077 | } |
1078 | |
1079 | @override |
1080 | void redepthChildren() => visitChildren(redepthChild); |
1081 | |
1082 | Alignment? _alignmentCache; |
1083 | Alignment get _resolvedAlignment => _alignmentCache ??= AlignmentDirectional.topStart.resolve(textDirection); |
1084 | |
1085 | void _markNeedResolution() { |
1086 | _alignmentCache = null; |
1087 | markNeedsLayout(); |
1088 | } |
1089 | |
1090 | TextDirection get textDirection => _textDirection; |
1091 | TextDirection _textDirection; |
1092 | set textDirection(TextDirection value) { |
1093 | if (_textDirection == value) { |
1094 | return; |
1095 | } |
1096 | _textDirection = value; |
1097 | _markNeedResolution(); |
1098 | } |
1099 | |
1100 | int get skipCount => _skipCount; |
1101 | int _skipCount; |
1102 | set skipCount(int value) { |
1103 | if (_skipCount != value) { |
1104 | _skipCount = value; |
1105 | markNeedsLayout(); |
1106 | } |
1107 | } |
1108 | |
1109 | /// {@macro flutter.material.Material.clipBehavior} |
1110 | /// |
1111 | /// Defaults to [Clip.hardEdge]. |
1112 | Clip get clipBehavior => _clipBehavior; |
1113 | Clip _clipBehavior = Clip.hardEdge; |
1114 | set clipBehavior(Clip value) { |
1115 | if (value != _clipBehavior) { |
1116 | _clipBehavior = value; |
1117 | markNeedsPaint(); |
1118 | markNeedsSemanticsUpdate(); |
1119 | } |
1120 | } |
1121 | |
1122 | // Adding/removing deferred child does not affect the layout of other children, |
1123 | // or that of the Overlay, so there's no need to invalidate the layout of the |
1124 | // Overlay. |
1125 | // |
1126 | // When _skipMarkNeedsLayout is true, markNeedsLayout does not do anything. |
1127 | bool _skipMarkNeedsLayout = false; |
1128 | void _addDeferredChild(_RenderDeferredLayoutBox child) { |
1129 | assert(!_skipMarkNeedsLayout); |
1130 | _skipMarkNeedsLayout = true; |
1131 | adoptChild(child); |
1132 | // The Overlay still needs repainting when a deferred child is added. Usually |
1133 | // `markNeedsLayout` implies `markNeedsPaint`, but here `markNeedsLayout` is |
1134 | // skipped when the `_skipMarkNeedsLayout` flag is set. |
1135 | markNeedsPaint(); |
1136 | _skipMarkNeedsLayout = false; |
1137 | |
1138 | // After adding `child` to the render tree, we want to make sure it will be |
1139 | // laid out in the same frame. This is done by calling markNeedsLayout on the |
1140 | // layout surrgate. This ensures `child` is reachable via tree walk (see |
1141 | // _RenderLayoutSurrogateProxyBox.performLayout). |
1142 | child._layoutSurrogate.markNeedsLayout(); |
1143 | } |
1144 | |
1145 | void _removeDeferredChild(_RenderDeferredLayoutBox child) { |
1146 | assert(!_skipMarkNeedsLayout); |
1147 | _skipMarkNeedsLayout = true; |
1148 | dropChild(child); |
1149 | // The Overlay still needs repainting when a deferred child is dropped. See |
1150 | // the comment in `_addDeferredChild`. |
1151 | markNeedsPaint(); |
1152 | _skipMarkNeedsLayout = false; |
1153 | } |
1154 | |
1155 | @override |
1156 | void markNeedsLayout() { |
1157 | if (!_skipMarkNeedsLayout) { |
1158 | super.markNeedsLayout(); |
1159 | } |
1160 | } |
1161 | |
1162 | RenderBox? get _firstOnstageChild { |
1163 | if (skipCount == super.childCount) { |
1164 | return null; |
1165 | } |
1166 | RenderBox? child = super.firstChild; |
1167 | for (int toSkip = skipCount; toSkip > 0; toSkip--) { |
1168 | final StackParentData childParentData = child!.parentData! as StackParentData; |
1169 | child = childParentData.nextSibling; |
1170 | assert(child != null); |
1171 | } |
1172 | return child; |
1173 | } |
1174 | |
1175 | RenderBox? get _lastOnstageChild => skipCount == super.childCount ? null : lastChild; |
1176 | |
1177 | @override |
1178 | double computeMinIntrinsicWidth(double height) { |
1179 | return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicWidth(height)); |
1180 | } |
1181 | |
1182 | @override |
1183 | double computeMaxIntrinsicWidth(double height) { |
1184 | return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMaxIntrinsicWidth(height)); |
1185 | } |
1186 | |
1187 | @override |
1188 | double computeMinIntrinsicHeight(double width) { |
1189 | return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicHeight(width)); |
1190 | } |
1191 | |
1192 | @override |
1193 | double computeMaxIntrinsicHeight(double width) { |
1194 | return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMaxIntrinsicHeight(width)); |
1195 | } |
1196 | |
1197 | @override |
1198 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |
1199 | assert(!debugNeedsLayout); |
1200 | double? result; |
1201 | RenderBox? child = _firstOnstageChild; |
1202 | while (child != null) { |
1203 | assert(!child.debugNeedsLayout); |
1204 | final StackParentData childParentData = child.parentData! as StackParentData; |
1205 | double? candidate = child.getDistanceToActualBaseline(baseline); |
1206 | if (candidate != null) { |
1207 | candidate += childParentData.offset.dy; |
1208 | if (result != null) { |
1209 | result = math.min(result, candidate); |
1210 | } else { |
1211 | result = candidate; |
1212 | } |
1213 | } |
1214 | child = childParentData.nextSibling; |
1215 | } |
1216 | return result; |
1217 | } |
1218 | |
1219 | @override |
1220 | Size computeDryLayout(BoxConstraints constraints) { |
1221 | if (constraints.biggest.isFinite) { |
1222 | return constraints.biggest; |
1223 | } |
1224 | return _findSizeDeterminingChild().getDryLayout(constraints); |
1225 | } |
1226 | |
1227 | @override |
1228 | // The following uses sync* because concurrent modifications should be allowed |
1229 | // during layout. |
1230 | Iterable<RenderBox> _childrenInPaintOrder() sync* { |
1231 | RenderBox? child = _firstOnstageChild; |
1232 | while (child != null) { |
1233 | yield child; |
1234 | final _TheaterParentData childParentData = child.parentData! as _TheaterParentData; |
1235 | final Iterator<RenderBox>? innerIterator = childParentData.paintOrderIterator; |
1236 | if (innerIterator != null) { |
1237 | while (innerIterator.moveNext()) { |
1238 | yield innerIterator.current; |
1239 | } |
1240 | } |
1241 | child = childParentData.nextSibling; |
1242 | } |
1243 | } |
1244 | |
1245 | @override |
1246 | // The following uses sync* because hit testing should be lazy. |
1247 | Iterable<RenderBox> _childrenInHitTestOrder() sync* { |
1248 | RenderBox? child = _lastOnstageChild; |
1249 | int childLeft = childCount - skipCount; |
1250 | while (child != null) { |
1251 | final _TheaterParentData childParentData = child.parentData! as _TheaterParentData; |
1252 | final Iterator<RenderBox>? innerIterator = childParentData.hitTestOrderIterator; |
1253 | if (innerIterator != null) { |
1254 | while (innerIterator.moveNext()) { |
1255 | yield innerIterator.current; |
1256 | } |
1257 | } |
1258 | yield child; |
1259 | childLeft -= 1; |
1260 | child = childLeft <= 0 ? null : childParentData.previousSibling; |
1261 | } |
1262 | } |
1263 | |
1264 | @override |
1265 | bool get sizedByParent => false; |
1266 | |
1267 | @override |
1268 | void performLayout() { |
1269 | RenderBox? sizeDeterminingChild; |
1270 | if (constraints.biggest.isFinite) { |
1271 | size = constraints.biggest; |
1272 | } else { |
1273 | sizeDeterminingChild = _findSizeDeterminingChild(); |
1274 | layoutChild(sizeDeterminingChild, constraints); |
1275 | size = sizeDeterminingChild.size; |
1276 | } |
1277 | |
1278 | // Equivalent to BoxConstraints used by RenderStack for StackFit.expand. |
1279 | final BoxConstraints nonPositionedChildConstraints = BoxConstraints.tight(size); |
1280 | for (final RenderBox child in _childrenInPaintOrder()) { |
1281 | if (child != sizeDeterminingChild) { |
1282 | layoutChild(child, nonPositionedChildConstraints); |
1283 | } |
1284 | } |
1285 | } |
1286 | |
1287 | RenderBox _findSizeDeterminingChild() { |
1288 | RenderBox? child = _lastOnstageChild; |
1289 | while (child != null) { |
1290 | final _TheaterParentData childParentData = child.parentData! as _TheaterParentData; |
1291 | if ((childParentData.overlayEntry?.canSizeOverlay ?? false) && !childParentData.isPositioned) { |
1292 | return child; |
1293 | } |
1294 | child = childParentData.previousSibling; |
1295 | } |
1296 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
1297 | ErrorSummary('Overlay was given infinite constraints and cannot be sized by a suitable child.' ), |
1298 | ErrorDescription( |
1299 | 'The constraints given to the overlay ( $constraints) would result in an illegal ' |
1300 | 'infinite size ( ${constraints.biggest}). To avoid that, the Overlay tried to size ' |
1301 | 'itself to one of its children, but no suitable non-positioned child that belongs to an ' |
1302 | 'OverlayEntry with canSizeOverlay set to true could be found.' , |
1303 | ), |
1304 | ErrorHint( |
1305 | 'Try wrapping the Overlay in a SizedBox to give it a finite size or ' |
1306 | 'use an OverlayEntry with canSizeOverlay set to true.' , |
1307 | ), |
1308 | ]); |
1309 | } |
1310 | |
1311 | final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
1312 | |
1313 | @override |
1314 | void paint(PaintingContext context, Offset offset) { |
1315 | if (clipBehavior != Clip.none) { |
1316 | _clipRectLayer.layer = context.pushClipRect( |
1317 | needsCompositing, |
1318 | offset, |
1319 | Offset.zero & size, |
1320 | super.paint, |
1321 | clipBehavior: clipBehavior, |
1322 | oldLayer: _clipRectLayer.layer, |
1323 | ); |
1324 | } else { |
1325 | _clipRectLayer.layer = null; |
1326 | super.paint(context, offset); |
1327 | } |
1328 | } |
1329 | |
1330 | @override |
1331 | void dispose() { |
1332 | _clipRectLayer.layer = null; |
1333 | super.dispose(); |
1334 | } |
1335 | |
1336 | @override |
1337 | void visitChildren(RenderObjectVisitor visitor) { |
1338 | RenderBox? child = firstChild; |
1339 | while (child != null) { |
1340 | visitor(child); |
1341 | final _TheaterParentData childParentData = child.parentData! as _TheaterParentData; |
1342 | childParentData.visitOverlayPortalChildrenOnOverlayEntry(visitor); |
1343 | child = childParentData.nextSibling; |
1344 | } |
1345 | } |
1346 | |
1347 | @override |
1348 | void visitChildrenForSemantics(RenderObjectVisitor visitor) { |
1349 | RenderBox? child = _firstOnstageChild; |
1350 | while (child != null) { |
1351 | visitor(child); |
1352 | final _TheaterParentData childParentData = child.parentData! as _TheaterParentData; |
1353 | child = childParentData.nextSibling; |
1354 | } |
1355 | } |
1356 | |
1357 | @override |
1358 | Rect? describeApproximatePaintClip(RenderObject child) { |
1359 | switch (clipBehavior) { |
1360 | case Clip.none: |
1361 | return null; |
1362 | case Clip.hardEdge: |
1363 | case Clip.antiAlias: |
1364 | case Clip.antiAliasWithSaveLayer: |
1365 | return Offset.zero & size; |
1366 | } |
1367 | } |
1368 | |
1369 | @override |
1370 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
1371 | super.debugFillProperties(properties); |
1372 | properties.add(IntProperty('skipCount' , skipCount)); |
1373 | properties.add(EnumProperty<TextDirection>('textDirection' , textDirection)); |
1374 | } |
1375 | |
1376 | @override |
1377 | List<DiagnosticsNode> debugDescribeChildren() { |
1378 | final List<DiagnosticsNode> offstageChildren = <DiagnosticsNode>[]; |
1379 | final List<DiagnosticsNode> onstageChildren = <DiagnosticsNode>[]; |
1380 | |
1381 | int count = 1; |
1382 | bool onstage = false; |
1383 | RenderBox? child = firstChild; |
1384 | final RenderBox? firstOnstageChild = _firstOnstageChild; |
1385 | while (child != null) { |
1386 | final _TheaterParentData childParentData = child.parentData! as _TheaterParentData; |
1387 | if (child == firstOnstageChild) { |
1388 | onstage = true; |
1389 | count = 1; |
1390 | } |
1391 | |
1392 | if (onstage) { |
1393 | onstageChildren.add( |
1394 | child.toDiagnosticsNode( |
1395 | name: 'onstage $count' , |
1396 | ), |
1397 | ); |
1398 | } else { |
1399 | offstageChildren.add( |
1400 | child.toDiagnosticsNode( |
1401 | name: 'offstage $count' , |
1402 | style: DiagnosticsTreeStyle.offstage, |
1403 | ), |
1404 | ); |
1405 | } |
1406 | |
1407 | int subcount = 1; |
1408 | childParentData.visitOverlayPortalChildrenOnOverlayEntry((RenderObject renderObject) { |
1409 | final RenderBox child = renderObject as RenderBox; |
1410 | if (onstage) { |
1411 | onstageChildren.add( |
1412 | child.toDiagnosticsNode( |
1413 | name: 'onstage $count - $subcount' , |
1414 | ), |
1415 | ); |
1416 | } else { |
1417 | offstageChildren.add( |
1418 | child.toDiagnosticsNode( |
1419 | name: 'offstage $count - $subcount' , |
1420 | style: DiagnosticsTreeStyle.offstage, |
1421 | ), |
1422 | ); |
1423 | } |
1424 | subcount += 1; |
1425 | }); |
1426 | |
1427 | child = childParentData.nextSibling; |
1428 | count += 1; |
1429 | } |
1430 | |
1431 | return <DiagnosticsNode>[ |
1432 | ...onstageChildren, |
1433 | if (offstageChildren.isNotEmpty) |
1434 | ...offstageChildren |
1435 | else |
1436 | DiagnosticsNode.message( |
1437 | 'no offstage children' , |
1438 | style: DiagnosticsTreeStyle.offstage, |
1439 | ), |
1440 | ]; |
1441 | } |
1442 | } |
1443 | |
1444 | |
1445 | // * OverlayPortal Implementation |
1446 | // OverlayPortal is inspired by the |
1447 | // [flutter_portal](https://pub.dev/packages/flutter_portal) package. |
1448 | // |
1449 | // ** RenderObject hierarchy |
1450 | // The widget works by inserting its overlay child's render subtree directly |
1451 | // under [Overlay]'s render object (_RenderTheater). |
1452 | // https://user-images.githubusercontent.com/31859944/171971838-62ed3975-4b5d-4733-a9c9-f79e263b8fcc.jpg |
1453 | // |
1454 | // To ensure the overlay child render subtree does not do layout twice, the |
1455 | // subtree must only perform layout after both its _RenderTheater and the |
1456 | // [OverlayPortal]'s render object (_RenderLayoutSurrogateProxyBox) have |
1457 | // finished layout. This is handled by _RenderDeferredLayoutBox. |
1458 | // |
1459 | // ** Z-Index of an overlay child |
1460 | // [_OverlayEntryLocation] is a (currently private) interface that allows an |
1461 | // [OverlayPortal] to insert its overlay child into a specific [Overlay], as |
1462 | // well as specifying the paint order between the overlay child and other |
1463 | // children of the _RenderTheater. |
1464 | // |
1465 | // Since [OverlayPortal] is only allowed to target ancestor [Overlay]s |
1466 | // (_RenderTheater must finish doing layout before _RenderDeferredLayoutBox), |
1467 | // the _RenderTheater should typically be acquired using an [InheritedWidget] |
1468 | // (currently, _RenderTheaterMarker) in case the [OverlayPortal] gets |
1469 | // reparented. |
1470 | |
1471 | /// A class to show, hide and bring to top an [OverlayPortal]'s overlay child |
1472 | /// in the target [Overlay]. |
1473 | /// |
1474 | /// A [OverlayPortalController] can only be given to at most one [OverlayPortal] |
1475 | /// at a time. When an [OverlayPortalController] is moved from one |
1476 | /// [OverlayPortal] to another, its [isShowing] state does not carry over. |
1477 | /// |
1478 | /// [OverlayPortalController.show] and [OverlayPortalController.hide] can be |
1479 | /// called even before the controller is assigned to any [OverlayPortal], but |
1480 | /// they typically should not be called while the widget tree is being rebuilt. |
1481 | class OverlayPortalController { |
1482 | /// Creates an [OverlayPortalController], optionally with a String identifier |
1483 | /// `debugLabel`. |
1484 | OverlayPortalController({ String? debugLabel }) : _debugLabel = debugLabel; |
1485 | |
1486 | _OverlayPortalState? _attachTarget; |
1487 | |
1488 | // A separate _zOrderIndex to allow `show()` or `hide()` to be called when the |
1489 | // controller is not yet attached. Once this controller is attached, |
1490 | // _attachTarget._zOrderIndex will be used as the source of truth, and this |
1491 | // variable will be set to null. |
1492 | int? _zOrderIndex; |
1493 | final String? _debugLabel; |
1494 | |
1495 | static int _wallTime = kIsWeb |
1496 | ? -9007199254740992 // -2^53 |
1497 | : -1 << 63; |
1498 | |
1499 | // Returns a unique and monotonically increasing timestamp that represents |
1500 | // now. |
1501 | // |
1502 | // The value this method returns increments after each call. |
1503 | int _now() { |
1504 | final int now = _wallTime += 1; |
1505 | assert(_zOrderIndex == null || _zOrderIndex! < now); |
1506 | assert(_attachTarget?._zOrderIndex == null || _attachTarget!._zOrderIndex! < now); |
1507 | return now; |
1508 | } |
1509 | |
1510 | /// Show the overlay child of the [OverlayPortal] this controller is attached |
1511 | /// to, at the top of the target [Overlay]. |
1512 | /// |
1513 | /// When there are more than one [OverlayPortal]s that target the same |
1514 | /// [Overlay], the overlay child of the last [OverlayPortal] to have called |
1515 | /// [show] appears at the top level, unobstructed. |
1516 | /// |
1517 | /// If [isShowing] is already true, calling this method brings the overlay |
1518 | /// child it controls to the top. |
1519 | /// |
1520 | /// This method should typically not be called while the widget tree is being |
1521 | /// rebuilt. |
1522 | void show() { |
1523 | final _OverlayPortalState? state = _attachTarget; |
1524 | if (state != null) { |
1525 | state.show(_now()); |
1526 | } else { |
1527 | _zOrderIndex = _now(); |
1528 | } |
1529 | } |
1530 | |
1531 | /// Hide the [OverlayPortal]'s overlay child. |
1532 | /// |
1533 | /// Once hidden, the overlay child will be removed from the widget tree the |
1534 | /// next time the widget tree rebuilds, and stateful widgets in the overlay |
1535 | /// child may lose states as a result. |
1536 | /// |
1537 | /// This method should typically not be called while the widget tree is being |
1538 | /// rebuilt. |
1539 | void hide() { |
1540 | final _OverlayPortalState? state = _attachTarget; |
1541 | if (state != null) { |
1542 | state.hide(); |
1543 | } else { |
1544 | assert(_zOrderIndex != null); |
1545 | _zOrderIndex = null; |
1546 | } |
1547 | } |
1548 | |
1549 | /// Whether the associated [OverlayPortal] should build and show its overlay |
1550 | /// child, using its `overlayChildBuilder`. |
1551 | bool get isShowing { |
1552 | final _OverlayPortalState? state = _attachTarget; |
1553 | return state != null |
1554 | ? state._zOrderIndex != null |
1555 | : _zOrderIndex != null; |
1556 | } |
1557 | |
1558 | /// Convenience method for toggling the current [isShowing] status. |
1559 | /// |
1560 | /// This method should typically not be called while the widget tree is being |
1561 | /// rebuilt. |
1562 | void toggle() => isShowing ? hide() : show(); |
1563 | |
1564 | @override |
1565 | String toString() { |
1566 | final String? debugLabel = _debugLabel; |
1567 | final String label = debugLabel == null ? '' : '( $debugLabel)' ; |
1568 | final String isDetached = _attachTarget != null ? '' : ' DETACHED' ; |
1569 | return ' ${objectRuntimeType(this, 'OverlayPortalController' )}$label$isDetached' ; |
1570 | }
|
1571 | }
|
1572 |
|
1573 | /// A widget that renders its overlay child on an [Overlay].
|
1574 | ///
|
1575 | /// The overlay child is initially hidden until [OverlayPortalController.show]
|
1576 | /// is called on the associated [controller]. The [OverlayPortal] uses
|
1577 | /// [overlayChildBuilder] to build its overlay child and renders it on the
|
1578 | /// specified [Overlay] as if it was inserted using an [OverlayEntry], while it
|
1579 | /// can depend on the same set of [InheritedWidget]s (such as [Theme]) that this
|
1580 | /// widget can depend on.
|
1581 | ///
|
1582 | /// This widget requires an [Overlay] ancestor in the widget tree when its
|
1583 | /// overlay child is showing. The overlay child is rendered by the [Overlay]
|
1584 | /// ancestor, not by the widget itself. This allows the overlay child to float
|
1585 | /// above other widgets, independent of its position in the widget tree.
|
1586 | ///
|
1587 | /// When [OverlayPortalController.hide] is called, the widget built using
|
1588 | /// [overlayChildBuilder] will be removed from the widget tree the next time the
|
1589 | /// widget rebuilds. Stateful descendants in the overlay child subtree may lose
|
1590 | /// states as a result.
|
1591 | ///
|
1592 | /// {@tool dartpad}
|
1593 | /// This example uses an [OverlayPortal] to build a tooltip that becomes visible
|
1594 | /// when the user taps on the [child] widget. There's a [DefaultTextStyle] above
|
1595 | /// the [OverlayPortal] controlling the [TextStyle] of both the [child] widget
|
1596 | /// and the widget [overlayChildBuilder] builds, which isn't otherwise doable if
|
1597 | /// the tooltip was added as an [OverlayEntry].
|
1598 | ///
|
1599 | /// ** See code in examples/api/lib/widgets/overlay/overlay_portal.0.dart **
|
1600 | /// {@end-tool}
|
1601 | ///
|
1602 | /// ### Paint Order
|
1603 | ///
|
1604 | /// In an [Overlay], an overlay child is painted after the [OverlayEntry]
|
1605 | /// associated with its [OverlayPortal] (that is, the [OverlayEntry] closest to
|
1606 | /// the [OverlayPortal] in the widget tree, which usually represents the
|
1607 | /// enclosing [Route]), and before the next [OverlayEntry].
|
1608 | ///
|
1609 | /// When an [OverlayEntry] has multiple associated [OverlayPortal]s, the paint
|
1610 | /// order between their overlay children is the order in which
|
1611 | /// [OverlayPortalController.show] was called. The last [OverlayPortal] to have
|
1612 | /// called `show` gets to paint its overlay child in the foreground.
|
1613 | ///
|
1614 | /// ### Semantics
|
1615 | ///
|
1616 | /// The semantics subtree generated by the overlay child is considered attached
|
1617 | /// to [OverlayPortal] instead of the target [Overlay]. An [OverlayPortal]'s
|
1618 | /// semantics subtree can be dropped from the semantics tree due to invisibility
|
1619 | /// while the overlay child is still visible (for example, when the
|
1620 | /// [OverlayPortal] is completely invisible in a [ListView] but kept alive by
|
1621 | /// a [KeepAlive] widget). When this happens the semantics subtree generated by
|
1622 | /// the overlay child is also dropped, even if the overlay child is still visible
|
1623 | /// on screen.
|
1624 | ///
|
1625 | /// {@template flutter.widgets.overlayPortalVsOverlayEntry}
|
1626 | /// ### Differences between [OverlayPortal] and [OverlayEntry]
|
1627 | ///
|
1628 | /// The main difference between [OverlayEntry] and [OverlayPortal] is that
|
1629 | /// [OverlayEntry] builds its widget subtree as a child of the target [Overlay],
|
1630 | /// while [OverlayPortal] uses [OverlayPortal.overlayChildBuilder] to build a
|
1631 | /// child widget of itself. This allows [OverlayPortal]'s overlay child to depend
|
1632 | /// on the same set of [InheritedWidget]s as [OverlayPortal], and it's also
|
1633 | /// guaranteed that the overlay child will not outlive its [OverlayPortal].
|
1634 | ///
|
1635 | /// On the other hand, [OverlayPortal]'s implementation is more complex. For
|
1636 | /// instance, it does a bit more work than a regular widget during global key
|
1637 | /// reparenting. If the content to be shown on the [Overlay] doesn't benefit
|
1638 | /// from being a part of [OverlayPortal]'s subtree, consider using an
|
1639 | /// [OverlayEntry] instead.
|
1640 | /// {@endtemplate}
|
1641 | ///
|
1642 | /// See also:
|
1643 | ///
|
1644 | /// * [OverlayEntry], an alternative API for inserting widgets into an
|
1645 | /// [Overlay].
|
1646 | /// * [Positioned], which can be used to size and position the overlay child in
|
1647 | /// relation to the target [Overlay]'s boundaries.
|
1648 | /// * [CompositedTransformFollower], which can be used to position the overlay
|
1649 | /// child in relation to the linked [CompositedTransformTarget] widget.
|
1650 | class OverlayPortal extends StatefulWidget {
|
1651 | /// Creates an [OverlayPortal] that renders the widget [overlayChildBuilder]
|
1652 | /// builds on the closest [Overlay] when [OverlayPortalController.show] is
|
1653 | /// called.
|
1654 | const OverlayPortal({
|
1655 | super.key,
|
1656 | required this.controller,
|
1657 | required this.overlayChildBuilder,
|
1658 | this.child,
|
1659 | }) : _targetRootOverlay = false;
|
1660 |
|
1661 | /// Creates an [OverlayPortal] that renders the widget [overlayChildBuilder]
|
1662 | /// builds on the root [Overlay] when [OverlayPortalController.show] is
|
1663 | /// called.
|
1664 | const OverlayPortal.targetsRootOverlay({
|
1665 | super.key,
|
1666 | required this.controller,
|
1667 | required this.overlayChildBuilder,
|
1668 | this.child,
|
1669 | }) : _targetRootOverlay = true;
|
1670 |
|
1671 | /// The controller to show, hide and bring to top the overlay child.
|
1672 | final OverlayPortalController controller;
|
1673 |
|
1674 | /// A [WidgetBuilder] used to build a widget below this widget in the tree,
|
1675 | /// that renders on the closest [Overlay].
|
1676 | ///
|
1677 | /// The said widget will only be built and shown in the closest [Overlay] once
|
1678 | /// [OverlayPortalController.show] is called on the associated [controller].
|
1679 | /// It will be painted in front of the [OverlayEntry] closest to this widget
|
1680 | /// in the widget tree (which is usually the enclosing [Route]).
|
1681 | ///
|
1682 | /// The built overlay child widget is inserted below this widget in the widget
|
1683 | /// tree, allowing it to depend on [InheritedWidget]s above it, and be
|
1684 | /// notified when the [InheritedWidget]s change.
|
1685 | ///
|
1686 | /// Unlike [child], the built overlay child can visually extend outside the
|
1687 | /// bounds of this widget without being clipped, and receive hit-test events
|
1688 | /// outside of this widget's bounds, as long as it does not extend outside of
|
1689 | /// the [Overlay] on which it is rendered.
|
1690 | final WidgetBuilder overlayChildBuilder;
|
1691 |
|
1692 | /// A widget below this widget in the tree.
|
1693 | final Widget? child;
|
1694 |
|
1695 | final bool _targetRootOverlay;
|
1696 |
|
1697 | @override
|
1698 | State<OverlayPortal> createState() => _OverlayPortalState();
|
1699 | }
|
1700 |
|
1701 | class _OverlayPortalState extends State<OverlayPortal> {
|
1702 | int? _zOrderIndex;
|
1703 | // The location of the overlay child within the overlay. This object will be
|
1704 | // used as the slot of the overlay child widget.
|
1705 | //
|
1706 | // The developer must call `show` to reveal the overlay so we can get a unique
|
1707 | // timestamp of the user interaction for determining the z-index of the
|
1708 | // overlay child in the overlay.
|
1709 | //
|
1710 | // Avoid invalidating the cache if possible, since the framework uses `==` to
|
1711 | // compare slots, and _OverlayEntryLocation can't override that operator since
|
1712 | // it's mutable. Changing slots can be relatively slow.
|
1713 | bool _childModelMayHaveChanged = true;
|
1714 | _OverlayEntryLocation? _locationCache;
|
1715 | static bool _isTheSameLocation(_OverlayEntryLocation locationCache, _RenderTheaterMarker marker) {
|
1716 | return locationCache._childModel == marker.overlayEntryWidgetState
|
1717 | && locationCache._theater == marker.theater;
|
1718 | }
|
1719 |
|
1720 | _OverlayEntryLocation _getLocation(int zOrderIndex, bool targetRootOverlay) {
|
1721 | final _OverlayEntryLocation? cachedLocation = _locationCache;
|
1722 | late final _RenderTheaterMarker marker = _RenderTheaterMarker.of(context, targetRootOverlay: targetRootOverlay);
|
1723 | final bool isCacheValid = cachedLocation != null
|
1724 | && (!_childModelMayHaveChanged || _isTheSameLocation(cachedLocation, marker));
|
1725 | _childModelMayHaveChanged = false;
|
1726 | if (isCacheValid) {
|
1727 | assert(cachedLocation._zOrderIndex == zOrderIndex);
|
1728 | assert(cachedLocation._debugIsLocationValid());
|
1729 | return cachedLocation;
|
1730 | }
|
1731 | // Otherwise invalidate the cache and create a new location.
|
1732 | cachedLocation?._debugMarkLocationInvalid();
|
1733 | final _OverlayEntryLocation newLocation = _OverlayEntryLocation(zOrderIndex, marker.overlayEntryWidgetState, marker.theater);
|
1734 | assert(newLocation._zOrderIndex == zOrderIndex);
|
1735 | return _locationCache = newLocation;
|
1736 | }
|
1737 |
|
1738 | @override
|
1739 | void initState() {
|
1740 | super.initState();
|
1741 | _setupController(widget.controller);
|
1742 | }
|
1743 |
|
1744 | void _setupController(OverlayPortalController controller) {
|
1745 | assert(
|
1746 | controller._attachTarget == null || controller._attachTarget == this,
|
1747 | 'Failed to attach $controller to $this. It is already attached to ${controller._attachTarget}.'
|
1748 | );
|
1749 | final int? controllerZOrderIndex = controller._zOrderIndex;
|
1750 | final int? zOrderIndex = _zOrderIndex;
|
1751 | if (zOrderIndex == null || (controllerZOrderIndex != null && controllerZOrderIndex > zOrderIndex)) {
|
1752 | _zOrderIndex = controllerZOrderIndex;
|
1753 | }
|
1754 | controller._zOrderIndex = null;
|
1755 | controller._attachTarget = this;
|
1756 | }
|
1757 |
|
1758 | @override
|
1759 | void didChangeDependencies() {
|
1760 | super.didChangeDependencies();
|
1761 | _childModelMayHaveChanged = true;
|
1762 | }
|
1763 |
|
1764 | @override
|
1765 | void didUpdateWidget(OverlayPortal oldWidget) {
|
1766 | super.didUpdateWidget(oldWidget);
|
1767 | _childModelMayHaveChanged = _childModelMayHaveChanged || oldWidget._targetRootOverlay != widget._targetRootOverlay;
|
1768 | if (oldWidget.controller != widget.controller) {
|
1769 | oldWidget.controller._attachTarget = null;
|
1770 | _setupController(widget.controller);
|
1771 | }
|
1772 | }
|
1773 |
|
1774 | @override
|
1775 | void dispose() {
|
1776 | assert(widget.controller._attachTarget == this);
|
1777 | widget.controller._attachTarget = null;
|
1778 | _locationCache?._debugMarkLocationInvalid();
|
1779 | _locationCache = null;
|
1780 | super.dispose();
|
1781 | }
|
1782 |
|
1783 | void show(int zOrderIndex) {
|
1784 | assert(
|
1785 | SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks,
|
1786 | ' ${widget.controller.runtimeType}.show() should not be called during build.'
|
1787 | );
|
1788 | setState(() { _zOrderIndex = zOrderIndex; });
|
1789 | _locationCache?._debugMarkLocationInvalid();
|
1790 | _locationCache = null;
|
1791 | }
|
1792 |
|
1793 | void hide() {
|
1794 | assert(SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks);
|
1795 | setState(() { _zOrderIndex = null; });
|
1796 | _locationCache?._debugMarkLocationInvalid();
|
1797 | _locationCache = null;
|
1798 | }
|
1799 |
|
1800 | @override
|
1801 | Widget build(BuildContext context) {
|
1802 | final int? zOrderIndex = _zOrderIndex;
|
1803 | if (zOrderIndex == null) {
|
1804 | return _OverlayPortal(
|
1805 | overlayLocation: null,
|
1806 | overlayChild: null,
|
1807 | child: widget.child,
|
1808 | );
|
1809 | }
|
1810 | return _OverlayPortal(
|
1811 | overlayLocation: _getLocation(zOrderIndex, widget._targetRootOverlay),
|
1812 | overlayChild: _DeferredLayout(child: Builder(builder: widget.overlayChildBuilder)),
|
1813 | child: widget.child,
|
1814 | );
|
1815 | }
|
1816 | }
|
1817 |
|
1818 | /// A location in an [Overlay].
|
1819 | ///
|
1820 | /// An [_OverlayEntryLocation] determines the [Overlay] the associated
|
1821 | /// [OverlayPortal] should put its overlay child onto, as well as the overlay
|
1822 | /// child's paint order in relation to other contents painted on the [Overlay].
|
1823 | //
|
1824 | // An _OverlayEntryLocation is a cursor pointing to a location in a particular
|
1825 | // Overlay's child model, and provides methods to insert/remove/move a
|
1826 | // _RenderDeferredLayoutBox to/from its target _theater.
|
1827 | //
|
1828 | // The occupant (a `RenderBox`) will be painted above the associated
|
1829 | // [OverlayEntry], but below the [OverlayEntry] above that [OverlayEntry].
|
1830 | //
|
1831 | // Additionally, `_activate` and `_deactivate` are called when the overlay
|
1832 | // child's `_OverlayPortalElement` activates/deactivates (for instance, during
|
1833 | // global key reparenting).
|
1834 | // `_OverlayPortalElement` removes its overlay child's render object from the
|
1835 | // target `_RenderTheater` when it deactivates and puts it back on `activated`.
|
1836 | // These 2 methods can be used to "hide" a child in the child model without
|
1837 | // removing it, when the child is expensive/difficult to re-insert at the
|
1838 | // correct location on `activated`.
|
1839 | //
|
1840 | // ### Equality
|
1841 | //
|
1842 | // An `_OverlayEntryLocation` will be used as an Element's slot. These 3 parts
|
1843 | // uniquely identify a place in an overlay's child model:
|
1844 | // - _theater
|
1845 | // - _childModel (the OverlayEntry)
|
1846 | // - _zOrderIndex
|
1847 | //
|
1848 | // Since it can't implement operator== (it's mutable), the same `_OverlayEntryLocation`
|
1849 | // instance must not be used to represent more than one locations.
|
1850 | final class _OverlayEntryLocation extends LinkedListEntry<_OverlayEntryLocation> {
|
1851 | _OverlayEntryLocation(this._zOrderIndex, this._childModel, this._theater);
|
1852 |
|
1853 | final int _zOrderIndex;
|
1854 | final _OverlayEntryWidgetState _childModel;
|
1855 | final _RenderTheater _theater;
|
1856 |
|
1857 | _RenderDeferredLayoutBox? _overlayChildRenderBox;
|
1858 | void _addToChildModel(_RenderDeferredLayoutBox child) {
|
1859 | assert(_overlayChildRenderBox == null, 'Failed to add $child. This location ( $this) is already occupied by $_overlayChildRenderBox.' );
|
1860 | _overlayChildRenderBox = child;
|
1861 | _childModel._add(this);
|
1862 | _theater.markNeedsPaint();
|
1863 | _theater.markNeedsCompositingBitsUpdate();
|
1864 | _theater.markNeedsSemanticsUpdate();
|
1865 | }
|
1866 | void _removeFromChildModel(_RenderDeferredLayoutBox child) {
|
1867 | assert(child == _overlayChildRenderBox);
|
1868 | _overlayChildRenderBox = null;
|
1869 | assert(_childModel._sortedTheaterSiblings?.contains(this) ?? false);
|
1870 | _childModel._remove(this);
|
1871 | _theater.markNeedsPaint();
|
1872 | _theater.markNeedsCompositingBitsUpdate();
|
1873 | _theater.markNeedsSemanticsUpdate();
|
1874 | }
|
1875 |
|
1876 | void _addChild(_RenderDeferredLayoutBox child) {
|
1877 | assert(_debugIsLocationValid());
|
1878 | _addToChildModel(child);
|
1879 | _theater._addDeferredChild(child);
|
1880 | assert(child.parent == _theater);
|
1881 | }
|
1882 |
|
1883 | void _removeChild(_RenderDeferredLayoutBox child) {
|
1884 | // This call is allowed even when this location is disposed.
|
1885 | _removeFromChildModel(child);
|
1886 | _theater._removeDeferredChild(child);
|
1887 | assert(child.parent == null);
|
1888 | }
|
1889 |
|
1890 | void _moveChild(_RenderDeferredLayoutBox child, _OverlayEntryLocation fromLocation) {
|
1891 | assert(fromLocation != this);
|
1892 | assert(_debugIsLocationValid());
|
1893 | final _RenderTheater fromTheater = fromLocation._theater;
|
1894 | final _OverlayEntryWidgetState fromModel = fromLocation._childModel;
|
1895 |
|
1896 | if (fromTheater != _theater) {
|
1897 | fromTheater._removeDeferredChild(child);
|
1898 | _theater._addDeferredChild(child);
|
1899 | }
|
1900 |
|
1901 | if (fromModel != _childModel || fromLocation._zOrderIndex != _zOrderIndex) {
|
1902 | fromLocation._removeFromChildModel(child);
|
1903 | _addToChildModel(child);
|
1904 | }
|
1905 | }
|
1906 |
|
1907 | void _activate(_RenderDeferredLayoutBox child) {
|
1908 | // This call is allowed even when this location is invalidated.
|
1909 | // See _OverlayPortalElement.activate.
|
1910 | assert(_overlayChildRenderBox == null, ' $_overlayChildRenderBox' );
|
1911 | _theater._addDeferredChild(child);
|
1912 | _overlayChildRenderBox = child;
|
1913 | }
|
1914 |
|
1915 | void _deactivate(_RenderDeferredLayoutBox child) {
|
1916 | // This call is allowed even when this location is invalidated.
|
1917 | _theater._removeDeferredChild(child);
|
1918 | _overlayChildRenderBox = null;
|
1919 | }
|
1920 |
|
1921 | // Throws a StateError if this location is already invalidated and shouldn't
|
1922 | // be used as an OverlayPortal slot. Must be used in asserts.
|
1923 | //
|
1924 | // Generally, `assert(_debugIsLocationValid())` should be used to prevent
|
1925 | // invalid accesses to an invalid `_OverlayEntryLocation` object. Exceptions
|
1926 | // to this rule are _removeChild, _deactive, which will be called when the
|
1927 | // OverlayPortal is being removed from the widget tree and may use the
|
1928 | // location information to perform cleanup tasks.
|
1929 | //
|
1930 | // Another exception is the _activate method which is called by
|
1931 | // _OverlayPortalElement.activate. See the comment in _OverlayPortalElement.activate.
|
1932 | bool _debugIsLocationValid() {
|
1933 | if (_debugMarkLocationInvalidStackTrace == null) {
|
1934 | return true;
|
1935 | }
|
1936 | throw StateError(' $this is already disposed. Stack trace: $_debugMarkLocationInvalidStackTrace' );
|
1937 | }
|
1938 |
|
1939 | // The StackTrace of the first _debugMarkLocationInvalid call. It's only for
|
1940 | // debugging purposes and the StackTrace will only be captured in debug builds.
|
1941 | //
|
1942 | // The effect of this method is not reversible. Once marked invalid, this
|
1943 | // object can't be marked as valid again.
|
1944 | StackTrace? _debugMarkLocationInvalidStackTrace;
|
1945 | @mustCallSuper
|
1946 | void _debugMarkLocationInvalid() {
|
1947 | assert(_debugIsLocationValid());
|
1948 | assert(() {
|
1949 | _debugMarkLocationInvalidStackTrace = StackTrace.current;
|
1950 | return true;
|
1951 | }());
|
1952 | }
|
1953 |
|
1954 | @override
|
1955 | String toString() => ' ${objectRuntimeType(this, '_OverlayEntryLocation' )}[ ${shortHash(this)}] ${_debugMarkLocationInvalidStackTrace != null ? "(INVALID)" :"" }' ;
|
1956 | }
|
1957 |
|
1958 | class _RenderTheaterMarker extends InheritedWidget {
|
1959 | const _RenderTheaterMarker({
|
1960 | required this.theater,
|
1961 | required this.overlayEntryWidgetState,
|
1962 | required super.child,
|
1963 | });
|
1964 |
|
1965 | final _RenderTheater theater;
|
1966 | final _OverlayEntryWidgetState overlayEntryWidgetState;
|
1967 |
|
1968 | @override
|
1969 | bool updateShouldNotify(_RenderTheaterMarker oldWidget) {
|
1970 | return oldWidget.theater != theater
|
1971 | || oldWidget.overlayEntryWidgetState != overlayEntryWidgetState;
|
1972 | }
|
1973 |
|
1974 | static _RenderTheaterMarker of(BuildContext context, { bool targetRootOverlay = false }) {
|
1975 | final _RenderTheaterMarker? marker;
|
1976 | if (targetRootOverlay) {
|
1977 | final InheritedElement? ancestor = _rootRenderTheaterMarkerOf(context.getElementForInheritedWidgetOfExactType<_RenderTheaterMarker>());
|
1978 | assert(ancestor == null || ancestor.widget is _RenderTheaterMarker);
|
1979 | marker = ancestor != null ? context.dependOnInheritedElement(ancestor) as _RenderTheaterMarker? : null;
|
1980 | } else {
|
1981 | marker = context.dependOnInheritedWidgetOfExactType<_RenderTheaterMarker>();
|
1982 | }
|
1983 | if (marker != null) {
|
1984 | return marker;
|
1985 | }
|
1986 | throw FlutterError.fromParts(<DiagnosticsNode>[
|
1987 | ErrorSummary('No Overlay widget found.' ),
|
1988 | ErrorDescription(
|
1989 | ' ${context.widget.runtimeType} widgets require an Overlay widget ancestor.\n'
|
1990 | 'An overlay lets widgets float on top of other widget children.' ,
|
1991 | ),
|
1992 | ErrorHint(
|
1993 | 'To introduce an Overlay widget, you can either directly '
|
1994 | 'include one, or use a widget that contains an Overlay itself, '
|
1995 | 'such as a Navigator, WidgetApp, MaterialApp, or CupertinoApp.' ,
|
1996 | ),
|
1997 | ...context.describeMissingAncestor(expectedAncestorType: Overlay),
|
1998 | ]);
|
1999 | }
|
2000 |
|
2001 | static InheritedElement? _rootRenderTheaterMarkerOf(InheritedElement? theaterMarkerElement) {
|
2002 | assert(theaterMarkerElement == null || theaterMarkerElement.widget is _RenderTheaterMarker);
|
2003 | if (theaterMarkerElement == null) {
|
2004 | return null;
|
2005 | }
|
2006 | InheritedElement? ancestor;
|
2007 | theaterMarkerElement.visitAncestorElements((Element element) {
|
2008 | ancestor = element.getElementForInheritedWidgetOfExactType<_RenderTheaterMarker>();
|
2009 | return false;
|
2010 | });
|
2011 | return ancestor == null ? theaterMarkerElement : _rootRenderTheaterMarkerOf(ancestor);
|
2012 | }
|
2013 | }
|
2014 |
|
2015 | class _OverlayPortal extends RenderObjectWidget {
|
2016 | /// Creates a widget that renders the given [overlayChild] in the [Overlay]
|
2017 | /// specified by `overlayLocation`.
|
2018 | ///
|
2019 | /// The `overlayLocation` parameter must not be null when [overlayChild] is not
|
2020 | /// null.
|
2021 | _OverlayPortal({
|
2022 | required this.overlayLocation,
|
2023 | required this.overlayChild,
|
2024 | required this.child,
|
2025 | }) : assert(overlayChild == null || overlayLocation != null),
|
2026 | assert(overlayLocation == null || overlayLocation._debugIsLocationValid());
|
2027 |
|
2028 | final Widget? overlayChild;
|
2029 |
|
2030 | /// A widget below this widget in the tree.
|
2031 | final Widget? child;
|
2032 |
|
2033 | final _OverlayEntryLocation? overlayLocation;
|
2034 |
|
2035 | @override
|
2036 | RenderObjectElement createElement() => _OverlayPortalElement(this);
|
2037 |
|
2038 | @override
|
2039 | RenderObject createRenderObject(BuildContext context) => _RenderLayoutSurrogateProxyBox();
|
2040 | }
|
2041 |
|
2042 | class _OverlayPortalElement extends RenderObjectElement {
|
2043 | _OverlayPortalElement(_OverlayPortal super.widget);
|
2044 |
|
2045 | @override
|
2046 | _RenderLayoutSurrogateProxyBox get renderObject => super.renderObject as _RenderLayoutSurrogateProxyBox;
|
2047 |
|
2048 | Element? _overlayChild;
|
2049 | Element? _child;
|
2050 |
|
2051 | @override
|
2052 | void mount(Element? parent, Object? newSlot) {
|
2053 | super.mount(parent, newSlot);
|
2054 | final _OverlayPortal widget = this.widget as _OverlayPortal;
|
2055 | _child = updateChild(_child, widget.child, null);
|
2056 | _overlayChild = updateChild(_overlayChild, widget.overlayChild, widget.overlayLocation);
|
2057 | }
|
2058 |
|
2059 | @override
|
2060 | void update(_OverlayPortal newWidget) {
|
2061 | super.update(newWidget);
|
2062 | _child = updateChild(_child, newWidget.child, null);
|
2063 | _overlayChild = updateChild(_overlayChild, newWidget.overlayChild, newWidget.overlayLocation);
|
2064 | }
|
2065 |
|
2066 | @override
|
2067 | void forgetChild(Element child) {
|
2068 | // The _overlayChild Element does not have a key because the _DeferredLayout
|
2069 | // widget does not take a Key, so only the regular _child can be taken
|
2070 | // during global key reparenting.
|
2071 | assert(child == _child);
|
2072 | _child = null;
|
2073 | super.forgetChild(child);
|
2074 | }
|
2075 |
|
2076 | @override
|
2077 | void visitChildren(ElementVisitor visitor) {
|
2078 | final Element? child = _child;
|
2079 | final Element? overlayChild = _overlayChild;
|
2080 | if (child != null) {
|
2081 | visitor(child);
|
2082 | }
|
2083 | if (overlayChild != null) {
|
2084 | visitor(overlayChild);
|
2085 | }
|
2086 | }
|
2087 |
|
2088 | @override
|
2089 | void activate() {
|
2090 | super.activate();
|
2091 | final Element? overlayChild = _overlayChild;
|
2092 | if (overlayChild != null) {
|
2093 | final _RenderDeferredLayoutBox? box = overlayChild.renderObject as _RenderDeferredLayoutBox?;
|
2094 | if (box != null) {
|
2095 | assert(!box.attached);
|
2096 | assert(renderObject._deferredLayoutChild == box);
|
2097 | // updateChild has not been called at this point so the RenderTheater in
|
2098 | // the overlay location could be detached. Adding children to a detached
|
2099 | // RenderObject is still allowed however this isn't the most efficient.
|
2100 | (overlayChild.slot! as _OverlayEntryLocation)._activate(box);
|
2101 | }
|
2102 | }
|
2103 | }
|
2104 |
|
2105 | @override
|
2106 | void deactivate() {
|
2107 | final Element? overlayChild = _overlayChild;
|
2108 | // Instead of just detaching the render objects, removing them from the
|
2109 | // render subtree entirely. This is a workaround for the
|
2110 | // !renderObject.attached assert in the `super.deactive()` method.
|
2111 | if (overlayChild != null) {
|
2112 | final _RenderDeferredLayoutBox? box = overlayChild.renderObject as _RenderDeferredLayoutBox?;
|
2113 | if (box != null) {
|
2114 | (overlayChild.slot! as _OverlayEntryLocation)._deactivate(box);
|
2115 | }
|
2116 | }
|
2117 | super.deactivate();
|
2118 | }
|
2119 |
|
2120 | @override
|
2121 | void insertRenderObjectChild(RenderBox child, _OverlayEntryLocation? slot) {
|
2122 | assert(child.parent == null, " $child's parent is not null: ${child.parent}" );
|
2123 | if (slot != null) {
|
2124 | renderObject._deferredLayoutChild = child as _RenderDeferredLayoutBox;
|
2125 | slot._addChild(child);
|
2126 | } else {
|
2127 | renderObject.child = child;
|
2128 | }
|
2129 | }
|
2130 |
|
2131 | // The [_DeferredLayout] widget does not have a key so there will be no
|
2132 | // reparenting between _overlayChild and _child, thus the non-null-typed slots.
|
2133 | @override
|
2134 | void moveRenderObjectChild(_RenderDeferredLayoutBox child, _OverlayEntryLocation oldSlot, _OverlayEntryLocation newSlot) {
|
2135 | assert(newSlot._debugIsLocationValid());
|
2136 | newSlot._moveChild(child, oldSlot);
|
2137 | }
|
2138 |
|
2139 | @override
|
2140 | void removeRenderObjectChild(RenderBox child, _OverlayEntryLocation? slot) {
|
2141 | if (slot == null) {
|
2142 | renderObject.child = null;
|
2143 | return;
|
2144 | }
|
2145 | assert(renderObject._deferredLayoutChild == child);
|
2146 | slot._removeChild(child as _RenderDeferredLayoutBox);
|
2147 | renderObject._deferredLayoutChild = null;
|
2148 | }
|
2149 |
|
2150 | @override
|
2151 | void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
2152 | super.debugFillProperties(properties);
|
2153 | properties.add(DiagnosticsProperty<Element>('child' , _child, defaultValue: null));
|
2154 | properties.add(DiagnosticsProperty<Element>('overlayChild' , _overlayChild, defaultValue: null));
|
2155 | properties.add(DiagnosticsProperty<Object>('overlayLocation' , _overlayChild?.slot, defaultValue: null));
|
2156 | }
|
2157 | }
|
2158 |
|
2159 | class _DeferredLayout extends SingleChildRenderObjectWidget {
|
2160 | const _DeferredLayout({
|
2161 | // This widget must not be given a key: we currently do not support
|
2162 | // reparenting between the overlayChild and child.
|
2163 | required Widget child,
|
2164 | }) : super(child: child);
|
2165 |
|
2166 | _RenderLayoutSurrogateProxyBox getLayoutParent(BuildContext context) {
|
2167 | return context.findAncestorRenderObjectOfType<_RenderLayoutSurrogateProxyBox>()!;
|
2168 | }
|
2169 |
|
2170 | @override
|
2171 | _RenderDeferredLayoutBox createRenderObject(BuildContext context) {
|
2172 | final _RenderLayoutSurrogateProxyBox parent = getLayoutParent(context);
|
2173 | final _RenderDeferredLayoutBox renderObject = _RenderDeferredLayoutBox(parent);
|
2174 | parent._deferredLayoutChild = renderObject;
|
2175 | return renderObject;
|
2176 | }
|
2177 |
|
2178 | @override
|
2179 | void updateRenderObject(BuildContext context, _RenderDeferredLayoutBox renderObject) {
|
2180 | assert(renderObject._layoutSurrogate == getLayoutParent(context));
|
2181 | assert(getLayoutParent(context)._deferredLayoutChild == renderObject);
|
2182 | }
|
2183 | }
|
2184 |
|
2185 | // A `RenderProxyBox` that defers its layout until its `_layoutSurrogate` (which
|
2186 | // is not necessarily an ancestor of this RenderBox, but shares at least one
|
2187 | // `_RenderTheater` ancestor with this RenderBox) is laid out.
|
2188 | //
|
2189 | // This `RenderObject` must be a child of a `_RenderTheater`. It guarantees that:
|
2190 | //
|
2191 | // 1. It's a relayout boundary, and `markParentNeedsLayout` is overridden such
|
2192 | // that it never dirties its `_RenderTheater`.
|
2193 | //
|
2194 | // 2. Its `layout` implementation is overridden such that `performLayout` does
|
2195 | // not do anything when its called from `layout`, preventing the parent
|
2196 | // `_RenderTheater` from laying out this subtree prematurely (but this
|
2197 | // `RenderObject` may still be resized). Instead, `markNeedsLayout` will be
|
2198 | // called from within `layout` to schedule a layout update for this relayout
|
2199 | // boundary when needed.
|
2200 | //
|
2201 | // 3. When invoked from `PipelineOwner.flushLayout`, or
|
2202 | // `_layoutSurrogate.performLayout`, this `RenderObject` behaves like an
|
2203 | // `Overlay` that has only one entry.
|
2204 | final class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterMixin, LinkedListEntry<_RenderDeferredLayoutBox> {
|
2205 | _RenderDeferredLayoutBox(this._layoutSurrogate);
|
2206 |
|
2207 | StackParentData get stackParentData => parentData! as StackParentData;
|
2208 | final _RenderLayoutSurrogateProxyBox _layoutSurrogate;
|
2209 |
|
2210 | @override
|
2211 | Iterable<RenderBox> _childrenInPaintOrder() {
|
2212 | final RenderBox? child = this.child;
|
2213 | return child == null
|
2214 | ? const Iterable<RenderBox>.empty()
|
2215 | : Iterable<RenderBox>.generate(1, (int i) => child);
|
2216 | }
|
2217 | @override
|
2218 | Iterable<RenderBox> _childrenInHitTestOrder() => _childrenInPaintOrder();
|
2219 |
|
2220 | @override
|
2221 | _RenderTheater get theater {
|
2222 | final RenderObject? parent = this.parent;
|
2223 | return parent is _RenderTheater
|
2224 | ? parent
|
2225 | : throw FlutterError(' $parent of $this is not a _RenderTheater' );
|
2226 | }
|
2227 |
|
2228 | @override
|
2229 | void redepthChildren() {
|
2230 | _layoutSurrogate.redepthChild(this);
|
2231 | super.redepthChildren();
|
2232 | }
|
2233 |
|
2234 | bool _callingMarkParentNeedsLayout = false;
|
2235 | @override
|
2236 | void markParentNeedsLayout() {
|
2237 | // No re-entrant calls.
|
2238 | if (_callingMarkParentNeedsLayout) {
|
2239 | return;
|
2240 | }
|
2241 | _callingMarkParentNeedsLayout = true;
|
2242 | markNeedsLayout();
|
2243 | _layoutSurrogate.markNeedsLayout();
|
2244 | _callingMarkParentNeedsLayout = false;
|
2245 | }
|
2246 |
|
2247 | @override
|
2248 | bool get sizedByParent => true;
|
2249 |
|
2250 | bool _needsLayout = true;
|
2251 | @override
|
2252 | void markNeedsLayout() {
|
2253 | _needsLayout = true;
|
2254 | super.markNeedsLayout();
|
2255 | }
|
2256 |
|
2257 | @override
|
2258 | RenderObject? get debugLayoutParent => _layoutSurrogate;
|
2259 |
|
2260 | void layoutByLayoutSurrogate() {
|
2261 | assert(!_theaterDoingThisLayout);
|
2262 | final _RenderTheater? theater = parent as _RenderTheater?;
|
2263 | if (theater == null || !attached) {
|
2264 | assert(false, ' $this is not attached to parent' );
|
2265 | return;
|
2266 | }
|
2267 | super.layout(BoxConstraints.tight(theater.constraints.biggest));
|
2268 | }
|
2269 |
|
2270 | bool _theaterDoingThisLayout = false;
|
2271 | @override
|
2272 | void layout(Constraints constraints, { bool parentUsesSize = false }) {
|
2273 | assert(_needsLayout == debugNeedsLayout);
|
2274 | // Only _RenderTheater calls this implementation.
|
2275 | assert(parent != null);
|
2276 | final bool scheduleDeferredLayout = _needsLayout || this.constraints != constraints;
|
2277 | assert(!_theaterDoingThisLayout);
|
2278 | _theaterDoingThisLayout = true;
|
2279 | super.layout(constraints, parentUsesSize: parentUsesSize);
|
2280 | assert(_theaterDoingThisLayout);
|
2281 | _theaterDoingThisLayout = false;
|
2282 | _needsLayout = false;
|
2283 | assert(!debugNeedsLayout);
|
2284 | if (scheduleDeferredLayout) {
|
2285 | final _RenderTheater parent = this.parent! as _RenderTheater;
|
2286 | // Invoking markNeedsLayout as a layout callback allows this node to be
|
2287 | // merged back to the `PipelineOwner`'s dirty list in the right order, if
|
2288 | // it's not already dirty. Otherwise this may cause some dirty descendants
|
2289 | // to performLayout a second time.
|
2290 | parent.invokeLayoutCallback((BoxConstraints constraints) { markNeedsLayout(); });
|
2291 | }
|
2292 | }
|
2293 |
|
2294 | @override
|
2295 | void performResize() {
|
2296 | size = constraints.biggest;
|
2297 | }
|
2298 |
|
2299 | bool _debugMutationsLocked = false;
|
2300 | @override
|
2301 | void performLayout() {
|
2302 | assert(!_debugMutationsLocked);
|
2303 | if (_theaterDoingThisLayout) {
|
2304 | _needsLayout = false;
|
2305 | return;
|
2306 | }
|
2307 | assert(() {
|
2308 | _debugMutationsLocked = true;
|
2309 | return true;
|
2310 | }());
|
2311 | // This method is directly being invoked from `PipelineOwner.flushLayout`,
|
2312 | // or from `_layoutSurrogate`'s performLayout.
|
2313 | assert(parent != null);
|
2314 | final RenderBox? child = this.child;
|
2315 | if (child == null) {
|
2316 | _needsLayout = false;
|
2317 | return;
|
2318 | }
|
2319 | assert(constraints.isTight);
|
2320 | layoutChild(child, constraints);
|
2321 | assert(() {
|
2322 | _debugMutationsLocked = false;
|
2323 | return true;
|
2324 | }());
|
2325 | _needsLayout = false;
|
2326 | }
|
2327 |
|
2328 | @override
|
2329 | void applyPaintTransform(RenderBox child, Matrix4 transform) {
|
2330 | final BoxParentData childParentData = child.parentData! as BoxParentData;
|
2331 | final Offset offset = childParentData.offset;
|
2332 | transform.translate(offset.dx, offset.dy);
|
2333 | }
|
2334 | }
|
2335 |
|
2336 | // A RenderProxyBox that makes sure its `deferredLayoutChild` has a greater
|
2337 | // depth than itself.
|
2338 | class _RenderLayoutSurrogateProxyBox extends RenderProxyBox {
|
2339 | _RenderDeferredLayoutBox? _deferredLayoutChild;
|
2340 |
|
2341 | @override
|
2342 | void redepthChildren() {
|
2343 | super.redepthChildren();
|
2344 | final _RenderDeferredLayoutBox? child = _deferredLayoutChild;
|
2345 | // If child is not attached, this method will be invoked by child's real
|
2346 | // parent when it's attached.
|
2347 | if (child != null && child.attached) {
|
2348 | assert(child.attached);
|
2349 | redepthChild(child);
|
2350 | }
|
2351 | }
|
2352 |
|
2353 | @override
|
2354 | void performLayout() {
|
2355 | super.performLayout();
|
2356 | // Try to layout `_deferredLayoutChild` here now that its configuration
|
2357 | // and constraints are up-to-date. Additionally, during the very first
|
2358 | // layout, this makes sure that _deferredLayoutChild is reachable via tree
|
2359 | // walk.
|
2360 | _deferredLayoutChild?.layoutByLayoutSurrogate();
|
2361 | }
|
2362 |
|
2363 | @override
|
2364 | void visitChildrenForSemantics(RenderObjectVisitor visitor) {
|
2365 | super.visitChildrenForSemantics(visitor);
|
2366 | final _RenderDeferredLayoutBox? deferredChild = _deferredLayoutChild;
|
2367 | if (deferredChild != null) {
|
2368 | visitor(deferredChild);
|
2369 | }
|
2370 | }
|
2371 | }
|
2372 |
|