diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index c34108e8549..e1cd6b1de08 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -2877,6 +2877,13 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge /// parentUsesSize ensures that this render object will undergo layout if the /// child undergoes layout. Otherwise, the child can change its layout /// information without informing this render object. + /// + /// Some special [RenderObject] subclasses (such as the one used by + /// [OverlayPortal.overlayChildLayoutBuilder]) call [applyPaintTransform] in + /// their [performLayout] implementation. To ensure such [RenderObject]s get + /// the up-to-date paint transform, [RenderObject] subclasses should typically + /// update the paint transform (as reported by [applyPaintTransform]) in this + /// method instead of [paint]. @protected void performLayout(); @@ -3494,8 +3501,9 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge /// Applies the transform that would be applied when painting the given child /// to the given matrix. /// - /// Used by coordinate conversion functions to translate coordinates local to - /// one render object into coordinates local to another render object. + /// Used by coordinate conversion functions ([getTransformTo], for example) to + /// translate coordinates local to one render object into coordinates local to + /// another render object. /// /// Some RenderObjects will provide a zeroed out matrix in this method, /// indicating that the child should not paint anything or respond to hit diff --git a/packages/flutter/lib/src/scheduler/binding.dart b/packages/flutter/lib/src/scheduler/binding.dart index 0bf3d0cb2ca..40558300190 100644 --- a/packages/flutter/lib/src/scheduler/binding.dart +++ b/packages/flutter/lib/src/scheduler/binding.dart @@ -577,13 +577,15 @@ mixin SchedulerBinding on BindingBase { /// Schedules the given transient frame callback. /// - /// Adds the given callback to the list of frame callbacks and ensures that a - /// frame is scheduled. + /// Adds the given callback to the list of frame callbacks, and ensures that a + /// frame is scheduled if the `scheduleNewFrame` argument is true. + /// + /// The `scheduleNewFrame` argument dictates whether [scheduleFrame] should be + /// called to ensure a new frame. Defaults to true. /// /// If this is called during the frame's animation phase (when transient frame - /// callbacks are still being invoked), a new frame will be scheduled, and - /// `callback` will be called in the newly scheduled frame, not in the current - /// frame. + /// callbacks are still being invoked), `callback` will be called in the next + /// frame, not in the current frame. /// /// If this is a one-off registration, ignore the `rescheduling` argument. /// @@ -605,8 +607,14 @@ mixin SchedulerBinding on BindingBase { /// * [WidgetsBinding.drawFrame], which explains the phases of each frame /// for those apps that use Flutter widgets (and where transient frame /// callbacks fit into those phases). - int scheduleFrameCallback(FrameCallback callback, {bool rescheduling = false}) { - scheduleFrame(); + int scheduleFrameCallback( + FrameCallback callback, { + bool rescheduling = false, + bool scheduleNewFrame = true, + }) { + if (scheduleNewFrame) { + scheduleFrame(); + } _nextFrameCallbackId += 1; _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry( callback, diff --git a/packages/flutter/lib/src/widgets/layout_builder.dart b/packages/flutter/lib/src/widgets/layout_builder.dart index 99f1204d607..7d40c10187e 100644 --- a/packages/flutter/lib/src/widgets/layout_builder.dart +++ b/packages/flutter/lib/src/widgets/layout_builder.dart @@ -19,37 +19,35 @@ typedef LayoutWidgetBuilder = Widget Function(BuildContext context, BoxConstrain /// An abstract superclass for widgets that defer their building until layout. /// -/// Similar to the [Builder] widget except that the framework calls the [builder] -/// function at layout time and provides the constraints that this widget should -/// adhere to. This is useful when the parent constrains the child's size and layout, -/// and doesn't depend on the child's intrinsic size. +/// Similar to the [Builder] widget except that the implementation calls the [builder] +/// function at layout time and provides the [LayoutInfoType] that is required to +/// configure the child widget subtree. /// -/// {@template flutter.widgets.ConstrainedLayoutBuilder} -/// The [builder] function is called in the following situations: +/// This is useful when the child widget tree relies on information that are only +/// available during layout, and doesn't depend on the child's intrinsic size. /// -/// * The first time the widget is laid out. -/// * When the parent widget passes different layout constraints. -/// * When the parent widget updates this widget. -/// * When the dependencies that the [builder] function subscribes to change. +/// The [LayoutInfoType] should typically be immutable. The equality of the +/// [LayoutInfoType] type is used by the implementation to avoid unnecessary +/// rebuilds: if the new [LayoutInfoType] computed during layout is the same as +/// (defined by `LayoutInfoType.==`) the previous [LayoutInfoType], the +/// implementation will try to avoid calling the [builder] again unless +/// [updateShouldRebuild] returns true. The corresponding [RenderObject] produced +/// by this widget retains the most up-to-date [LayoutInfoType] for this purpose, +/// which may keep a [LayoutInfoType] object in memory until the widget is removed +/// from the tree. /// -/// The [builder] function is _not_ called during layout if the parent passes -/// the same constraints repeatedly. -/// {@endtemplate} -/// -/// Subclasses must return a [RenderObject] that mixes in -/// [RenderConstrainedLayoutBuilder]. -abstract class ConstrainedLayoutBuilder - extends RenderObjectWidget { +/// Subclasses must return a [RenderObject] that mixes in [RenderAbstractLayoutBuilderMixin]. +abstract class AbstractLayoutBuilder extends RenderObjectWidget { /// Creates a widget that defers its building until layout. - const ConstrainedLayoutBuilder({super.key, required this.builder}); - - @override - RenderObjectElement createElement() => _LayoutBuilderElement(this); + const AbstractLayoutBuilder({super.key}); /// Called at layout time to construct the widget tree. /// /// The builder must not return null. - final Widget Function(BuildContext context, ConstraintType constraints) builder; + Widget Function(BuildContext context, LayoutInfoType layoutInfo) get builder; + + @override + RenderObjectElement createElement() => _LayoutBuilderElement(this); /// Whether [builder] needs to be called again even if the layout constraints /// are the same. @@ -71,17 +69,45 @@ abstract class ConstrainedLayoutBuilder /// * [Element.update], the method that actually updates the widget's /// configuration. @protected - bool updateShouldRebuild(covariant ConstrainedLayoutBuilder oldWidget) => true; + bool updateShouldRebuild(covariant AbstractLayoutBuilder oldWidget) => true; + + @override + RenderAbstractLayoutBuilderMixin createRenderObject( + BuildContext context, + ); // updateRenderObject is redundant with the logic in the LayoutBuilderElement below. } -class _LayoutBuilderElement extends RenderObjectElement { - _LayoutBuilderElement(ConstrainedLayoutBuilder super.widget); +/// A specialized [AbstractLayoutBuilder] whose widget subtree depends on the +/// incoming [ConstraintType] that will be imposed on the widget. +/// +/// {@template flutter.widgets.ConstrainedLayoutBuilder} +/// The [builder] function is called in the following situations: +/// +/// * The first time the widget is laid out. +/// * When the parent widget passes different layout constraints. +/// * When the parent widget updates this widget and [updateShouldRebuild] returns `true`. +/// * When the dependencies that the [builder] function subscribes to change. +/// +/// The [builder] function is _not_ called during layout if the parent passes +/// the same constraints repeatedly. +/// {@endtemplate} +abstract class ConstrainedLayoutBuilder + extends AbstractLayoutBuilder { + /// Creates a widget that defers its building until layout. + const ConstrainedLayoutBuilder({super.key, required this.builder}); @override - RenderConstrainedLayoutBuilder get renderObject => - super.renderObject as RenderConstrainedLayoutBuilder; + final Widget Function(BuildContext context, ConstraintType constraints) builder; +} + +class _LayoutBuilderElement extends RenderObjectElement { + _LayoutBuilderElement(AbstractLayoutBuilder super.widget); + + @override + RenderAbstractLayoutBuilderMixin get renderObject => + super.renderObject as RenderAbstractLayoutBuilderMixin; Element? _child; @@ -140,18 +166,18 @@ class _LayoutBuilderElement extends RenderOb @override void mount(Element? parent, Object? newSlot) { super.mount(parent, newSlot); // Creates the renderObject. - renderObject.updateCallback(_rebuildWithConstraints); + renderObject._updateCallback(_rebuildWithConstraints); } @override - void update(ConstrainedLayoutBuilder newWidget) { + void update(AbstractLayoutBuilder newWidget) { assert(widget != newWidget); - final ConstrainedLayoutBuilder oldWidget = - widget as ConstrainedLayoutBuilder; + final AbstractLayoutBuilder oldWidget = + widget as AbstractLayoutBuilder; super.update(newWidget); assert(widget == newWidget); - renderObject.updateCallback(_rebuildWithConstraints); + renderObject._updateCallback(_rebuildWithConstraints); if (newWidget.updateShouldRebuild(oldWidget)) { _needsBuild = true; renderObject.markNeedsLayout(); @@ -183,22 +209,24 @@ class _LayoutBuilderElement extends RenderOb @override void unmount() { - renderObject.updateCallback(null); + renderObject._updateCallback(null); super.unmount(); } - // The constraints that were passed to this class last time it was laid out. - // These constraints are compared to the new constraints to determine whether - // [ConstrainedLayoutBuilder.builder] needs to be called. - ConstraintType? _previousConstraints; + // The LayoutInfoType that was used to invoke the layout callback with last time, + // during layout. The `_previousLayoutInfo` value is compared to the new one + // to determine whether [LayoutBuilderBase.builder] needs to be called. + LayoutInfoType? _previousLayoutInfo; bool _needsBuild = true; - void _rebuildWithConstraints(ConstraintType constraints) { + void _rebuildWithConstraints(Constraints _) { + final LayoutInfoType layoutInfo = renderObject.layoutInfo; @pragma('vm:notify-debugger-on-exception') void updateChildCallback() { Widget built; try { - built = (widget as ConstrainedLayoutBuilder).builder(this, constraints); + assert(layoutInfo == renderObject.layoutInfo); + built = (widget as AbstractLayoutBuilder).builder(this, layoutInfo); debugWidgetBuilderValue(widget, built); } catch (e, stack) { built = ErrorWidget.builder( @@ -231,12 +259,12 @@ class _LayoutBuilderElement extends RenderOb _child = updateChild(null, built, slot); } finally { _needsBuild = false; - _previousConstraints = constraints; + _previousLayoutInfo = layoutInfo; } } final VoidCallback? callback = - _needsBuild || (constraints != _previousConstraints) ? updateChildCallback : null; + _needsBuild || (layoutInfo != _previousLayoutInfo) ? updateChildCallback : null; owner!.buildScope(this, callback); } @@ -256,7 +284,7 @@ class _LayoutBuilderElement extends RenderOb @override void removeRenderObjectChild(RenderObject child, Object? slot) { - final RenderConstrainedLayoutBuilder renderObject = + final RenderAbstractLayoutBuilderMixin renderObject = this.renderObject; assert(renderObject.child == child); renderObject.child = null; @@ -264,19 +292,21 @@ class _LayoutBuilderElement extends RenderOb } } -/// Generic mixin for [RenderObject]s created by [ConstrainedLayoutBuilder]. +/// Generic mixin for [RenderObject]s created by an [AbstractLayoutBuilder] with +/// the the same `LayoutInfoType`. /// -/// Provides a callback that should be called at layout time, typically in -/// [RenderObject.performLayout]. -mixin RenderConstrainedLayoutBuilder< - ConstraintType extends Constraints, - ChildType extends RenderObject -> +/// Provides a [rebuildIfNecessary] method that should be called at layout time, +/// typically in [RenderObject.performLayout]. The method invokes +/// [AbstractLayoutBuilder]'s builder callback if needed. +/// +/// Implementers must provide a [layoutInfo] implementation that is safe to +/// access in [rebuildIfNecessary], which is typically called in [performLayout]. +mixin RenderAbstractLayoutBuilderMixin on RenderObjectWithChildMixin { - LayoutCallback? _callback; + LayoutCallback? _callback; /// Change the layout callback. - void updateCallback(LayoutCallback? value) { + void _updateCallback(LayoutCallback? value) { if (value == _callback) { return; } @@ -284,14 +314,28 @@ mixin RenderConstrainedLayoutBuilder< markNeedsLayout(); } - /// Invoke the callback supplied via [updateCallback]. + /// Invoke the builder callback supplied via [AbstractLayoutBuilder] and + /// rebuilds the [AbstractLayoutBuilder]'s widget tree, if needed. /// - /// Typically this results in [ConstrainedLayoutBuilder.builder] being called - /// during layout. + /// No work will be done if [layoutInfo] has not changed since the last time + /// this method was called, and [AbstractLayoutBuilder.updateShouldRebuild] + /// returned `false` when the widget was rebuilt. + /// + /// This method should typically be called as soon as possible in the class's + /// [performLayout] implementation, before any layout work is done. + @protected void rebuildIfNecessary() { assert(_callback != null); invokeLayoutCallback(_callback!); } + + /// The information to invoke the [AbstractLayoutBuilder.builder] callback with. + /// + /// This is typically the information that are only made available in + /// [performLayout], which is inaccessible for regular [Builder] widget, + /// such as the incoming [Constraints]. + @protected + LayoutInfoType get layoutInfo; } /// Builds a widget tree that can depend on the parent widget's size. @@ -329,13 +373,15 @@ class LayoutBuilder extends ConstrainedLayoutBuilder { const LayoutBuilder({super.key, required super.builder}); @override - RenderObject createRenderObject(BuildContext context) => _RenderLayoutBuilder(); + RenderAbstractLayoutBuilderMixin createRenderObject( + BuildContext context, + ) => _RenderLayoutBuilder(); } class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin, - RenderConstrainedLayoutBuilder { + RenderAbstractLayoutBuilderMixin { @override double computeMinIntrinsicWidth(double height) { assert(_debugThrowIfNotCheckingIntrinsics()); @@ -428,6 +474,10 @@ class _RenderLayoutBuilder extends RenderBox return true; } + + @protected + @override + BoxConstraints get layoutInfo => constraints; } FlutterErrorDetails _reportException( diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index 805947f26dc..c75b22032c4 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -24,9 +24,31 @@ import 'package:flutter/scheduler.dart'; import 'basic.dart'; import 'framework.dart'; +import 'layout_builder.dart'; import 'lookup_boundary.dart'; import 'ticker_provider.dart'; +/// The signature of the widget builder callback used in +/// [OverlayPortal.overlayChildLayoutBuilder]. +typedef OverlayChildLayoutBuilder = + Widget Function(BuildContext context, OverlayChildLayoutInfo info); + +/// The additional layout information available to the +/// [OverlayPortal.overlayChildLayoutBuilder] callback. +extension type OverlayChildLayoutInfo._( + (Size childSize, Matrix4 childPaintTransform, Size overlaySize) _info +) { + /// The size of [OverlayPortal.child] in its own coordinates. + Size get childSize => _info.$1; + + /// The paint transform of [OverlayPortal.child], in the target [Overlay]'s + /// coordinates. + Matrix4 get childPaintTransform => _info.$2; + + /// The size of the target [Overlay] in its own coordinates. + Size get overlaySize => _info.$3; +} + // Examples can assume: // late BuildContext context; @@ -337,21 +359,25 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> { // mutated. The reason for that is it's allowed to add/remove/move deferred // children to a _RenderTheater during performLayout, but the affected // children don't have to be laid out in the same performLayout call. - late final Iterable _paintOrderIterable = _createChildIterable(reversed: false); + late final Iterable<_RenderDeferredLayoutBox> _paintOrderIterable = _createChildIterable( + reversed: false, + ); // An Iterable that traverse the children in the child model in // hit-test order (from closest to the user to the farthest to the user). - late final Iterable _hitTestOrderIterable = _createChildIterable(reversed: true); + late final Iterable<_RenderDeferredLayoutBox> _hitTestOrderIterable = _createChildIterable( + reversed: true, + ); // The following uses sync* because hit-testing is lazy, and LinkedList as a // Iterable doesn't support concurrent modification. - Iterable _createChildIterable({required bool reversed}) sync* { + Iterable<_RenderDeferredLayoutBox> _createChildIterable({required bool reversed}) sync* { final LinkedList<_OverlayEntryLocation>? children = _sortedTheaterSiblings; if (children == null || children.isEmpty) { return; } _OverlayEntryLocation? candidate = reversed ? children.last : children.first; while (candidate != null) { - final RenderBox? renderBox = candidate._overlayChildRenderBox; + final _RenderDeferredLayoutBox? renderBox = candidate._overlayChildRenderBox; candidate = reversed ? candidate.previous : candidate.next; if (renderBox != null) { yield renderBox; @@ -982,8 +1008,10 @@ class _TheaterElement extends MultiChildRenderObjectElement { super.moveRenderObjectChild(child, oldSlot, newSlot); assert(() { final _TheaterParentData parentData = child.parentData! as _TheaterParentData; - return parentData.overlayEntry == + final OverlayEntry entryAtNewSlot = ((widget as _Theater).children[newSlot.index] as _OverlayEntryWidget).entry; + assert(parentData.overlayEntry == entryAtNewSlot); + return true; }()); } @@ -1113,9 +1141,9 @@ class _TheaterParentData extends StackParentData { // _overlayStateMounted is set to null in _OverlayEntryWidgetState's dispose // method. This property is only accessed during layout, paint and hit-test so // the `value!` should be safe. - Iterator? get paintOrderIterator => + Iterator<_RenderDeferredLayoutBox>? get paintOrderIterator => overlayEntry?._overlayEntryStateNotifier?.value!._paintOrderIterable.iterator; - Iterator? get hitTestOrderIterator => + Iterator<_RenderDeferredLayoutBox>? get hitTestOrderIterator => overlayEntry?._overlayEntryStateNotifier?.value!._hitTestOrderIterable.iterator; // A convenience method for traversing `paintOrderIterator` with a @@ -1239,7 +1267,7 @@ class _RenderTheater extends RenderBox // After adding `child` to the render tree, we want to make sure it will be // laid out in the same frame. This is done by calling markNeedsLayout on the - // layout surrogate. This ensures `child` is reachable via tree walk (see + // layout surrogate. This ensures `child` is added to the dirty list (see // _RenderLayoutSurrogateProxyBox.performLayout). child._layoutSurrogate.markNeedsLayout(); } @@ -1776,6 +1804,39 @@ class OverlayPortal extends StatefulWidget { this.child, }) : _targetRootOverlay = true; + /// Creates an [OverlayPortal] that renders the widget `overlayChildBuilder` + /// builds on the closest [Overlay] when [OverlayPortalController.show] is + /// called. + /// + /// Developers can use `overlayChildBuilder` to configure the overlay child + /// based on the the size and the location of [OverlayPortal.child] within the + /// target [Overlay], as well as the size of the [Overlay] itself. This allows + /// the overlay child to, for example, always follow [OverlayPortal.child] and + /// at the same time resize itself base on how close it is to the edges of + /// the [Overlay]. + /// + /// The `overlayChildBuilder` callback is called during layout. To ensure the + /// paint transform of [OverlayPortal.child] in relation to the target + /// [Overlay] is up-to-date by then, all [RenderObject]s between the + /// [OverlayPortal] to the target [Overlay] must establish their paint + /// transform during the layout phase, which most [RenderObject]s do. One + /// exception is the [CompositedTransformFollower] widget, whose [RenderObject] + /// only establishes the paint transform when composited. Putting a + /// [CompositedTransformFollower] between the [OverlayPortal] and the [Overlay] + /// may resulting in an incorrect child paint transform being provided to the + /// `overlayChildBuilder` and will cause an assertion in debug mode. + OverlayPortal.overlayChildLayoutBuilder({ + Key? key, + required OverlayPortalController controller, + required OverlayChildLayoutBuilder overlayChildBuilder, + required Widget? child, + }) : this( + key: key, + controller: controller, + overlayChildBuilder: (_) => _OverlayChildLayoutBuilder(builder: overlayChildBuilder), + child: child, + ); + /// The controller to show, hide and bring to top the overlay child. final OverlayPortalController controller; @@ -2313,14 +2374,16 @@ class _DeferredLayout extends SingleChildRenderObjectWidget { } } -// A `RenderProxyBox` that defers its layout until its `_layoutSurrogate` (which -// is not necessarily an ancestor of this RenderBox, but shares at least one -// `_RenderTheater` ancestor with this RenderBox) is laid out. +// This `RenderObject` must be a child of a `_RenderTheater`. It guarantees that +// it only does layout after the sizes of the render objects from its +// `_layoutSurrogate` (which must be a descendant of this `RenderObject`'s +// parent) through the parent `_RenderTheater` are known. To this end: // -// This `RenderObject` must be a child of a `_RenderTheater`. It guarantees that: -// -// 1. It's a relayout boundary, so calling `markNeedsLayout` on it never dirties -// its `_RenderTheater`. +// 1. It's a relayout boundary, and calling `markNeedsLayout` on it or adding it +// to the `_RenderTheater` as a child never dirties its `_RenderTheater`. +// Instead, it is always added to the `PipelineOwner`'s dirty list when it +// needs layout (even for the initial layout when it is first added to the +// tree). // // 2. Its `layout` implementation is overridden such that `performLayout` does // not do anything when its called from `layout`, preventing the parent @@ -2329,9 +2392,8 @@ class _DeferredLayout extends SingleChildRenderObjectWidget { // called from within `layout` to schedule a layout update for this relayout // boundary when needed. // -// 3. When invoked from `PipelineOwner.flushLayout`, or -// `_layoutSurrogate.performLayout`, this `RenderObject` behaves like an -// `Overlay` that has only one entry. +// When invoked from `PipelineOwner.flushLayout`, this `RenderObject` behaves +// like an `Overlay` that has only one entry. final class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterMixin, LinkedListEntry<_RenderDeferredLayoutBox> { _RenderDeferredLayoutBox(this._layoutSurrogate); @@ -2351,12 +2413,10 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox Iterable _childrenInHitTestOrder() => _childrenInPaintOrder(); @override - _RenderTheater get theater { - final RenderObject? parent = this.parent; - return parent is _RenderTheater - ? parent - : throw FlutterError('$parent of $this is not a _RenderTheater'); - } + _RenderTheater get theater => switch (parent) { + final _RenderTheater parent => parent, + _ => throw FlutterError('$parent of $this is not a _RenderTheater'), + }; @override void redepthChildren() { @@ -2367,6 +2427,11 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox @override bool get sizedByParent => true; + bool get needsLayout { + assert(debugNeedsLayout == _needsLayout); + return _needsLayout; + } + bool _needsLayout = true; @override void markNeedsLayout() { @@ -2395,53 +2460,47 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox @override RenderObject? get debugLayoutParent => _layoutSurrogate; - void layoutByLayoutSurrogate() { - assert(!_theaterDoingThisLayout); - final _RenderTheater? theater = parent as _RenderTheater?; - if (theater == null || !attached) { - assert(false, '$this is not attached to parent'); - return; - } - if (theater._layingOutSizeDeterminingChild) { - theater.invokeLayoutCallback((BoxConstraints constraints) { + /// Whether this RenderBox's layout method is currently being called by the + /// theater or the layoutSurrogate's [performLayout] implementation. + bool _doingLayoutFromTreeWalk = false; + void _doLayoutFrom(RenderObject treewalkParent, {required Constraints constraints}) { + final bool shouldAddToDirtyList = needsLayout || this.constraints != constraints; + assert(!_doingLayoutFromTreeWalk); + _doingLayoutFromTreeWalk = true; + super.layout(constraints); + assert(_doingLayoutFromTreeWalk); + _doingLayoutFromTreeWalk = false; + _needsLayout = false; + assert(!debugNeedsLayout); + if (shouldAddToDirtyList) { + // Instead of laying out this subtree via treewalk, adding it to the dirty + // list. This ensures: + // + // 1. this node will be laid out by the PipelineOwner *after* the two + // nodes it depends on (the theater and the layout surrogate) are + // laid out, as it has a greater depth value than its dependencies. + // + // 2. when the deferred child's child starts to do layout, the nodes + // from the layout surrogate to the theater (exclusive) have finishd + // doing layout, so the deferred child's child can read their sizes + // and (usually) compute the paint transform of the regular child + // within the Overlay. + // + // Invoking markNeedsLayout as a layout callback allows this node to be + // merged back to the `PipelineOwner`'s dirty list in the right order, if + // it's not already dirty, such that this subtree does not get laid out + // twice. + treewalkParent.invokeLayoutCallback((BoxConstraints _) { markNeedsLayout(); }); - } else { - final BoxConstraints theaterConstraints = theater.constraints; - final Size boxSize = - theaterConstraints.biggest.isFinite - ? theaterConstraints.biggest - // Accessing the theater's size is only unsafe if it is laying out the - // size-determining child. - : theater.size; - super.layout(BoxConstraints.tight(boxSize)); } } - bool _theaterDoingThisLayout = false; @override void layout(Constraints constraints, {bool parentUsesSize = false}) { - assert(_needsLayout == debugNeedsLayout); - // Only _RenderTheater calls this implementation. - assert(parent != null); - final bool scheduleDeferredLayout = _needsLayout || this.constraints != constraints; - assert(!_theaterDoingThisLayout); - _theaterDoingThisLayout = true; - super.layout(constraints, parentUsesSize: parentUsesSize); - assert(_theaterDoingThisLayout); - _theaterDoingThisLayout = false; - _needsLayout = false; - assert(!debugNeedsLayout); - if (scheduleDeferredLayout) { - final _RenderTheater parent = this.parent! as _RenderTheater; - // Invoking markNeedsLayout as a layout callback allows this node to be - // merged back to the `PipelineOwner`'s dirty list in the right order, if - // it's not already dirty. Otherwise this may cause some dirty descendants - // to performLayout a second time. - parent.invokeLayoutCallback((BoxConstraints constraints) { - markNeedsLayout(); - }); - } + // The `parentUsesSize` flag can be safely ignored since this render box is + // sized by the parent. + _doLayoutFrom(parent!, constraints: constraints); } @override @@ -2453,7 +2512,7 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox @override void performLayout() { assert(!_debugMutationsLocked); - if (_theaterDoingThisLayout) { + if (_doingLayoutFromTreeWalk) { _needsLayout = false; return; } @@ -2495,10 +2554,9 @@ class _RenderLayoutSurrogateProxyBox extends RenderProxyBox { void redepthChildren() { super.redepthChildren(); final _RenderDeferredLayoutBox? child = _deferredLayoutChild; - // If child is not attached, this method will be invoked by child's real - // parent when it's attached. + // If child is not attached yet, this method will be invoked by child's real + // parent (the theater) when it becomes attached. if (child != null && child.attached) { - assert(child.attached); redepthChild(child); } } @@ -2506,11 +2564,33 @@ class _RenderLayoutSurrogateProxyBox extends RenderProxyBox { @override void performLayout() { super.performLayout(); - // Try to layout `_deferredLayoutChild` here now that its configuration - // and constraints are up-to-date. Additionally, during the very first - // layout, this makes sure that _deferredLayoutChild is reachable via tree - // walk. - _deferredLayoutChild?.layoutByLayoutSurrogate(); + final _RenderDeferredLayoutBox? deferredChild = _deferredLayoutChild; + if (deferredChild == null) { + return; + } + // To make sure all ancestors' performLayout calls have returned when + // the deferred child does layout, the deferred child needs to be put in + // the dirty list if it is dirty, and make the deferred child subtree + // unreachable via layout tree walk. + // + // The deferred child is guaranteed to be a relayout boundary but it may + // still not be in the dirty list if it has never been laid out before + // (its _relayoutBoundary is unknown to the framework so it's not treated as + // one). The code below handles this case and makes sure the deferred child + // is in the dirty list. + final _RenderTheater theater = deferredChild.parent! as _RenderTheater; + // If the theater is laying out the size-determining child, its size is not + // available yet. Since the theater always lays out the size-determining + // child first and the deferred child can never be size-determining, + // this method does not have to do anything, the theater will update the + // constraints of the deferred child and resize / put it in the dirty list if + // needed. + if (!theater._layingOutSizeDeterminingChild) { + final BoxConstraints theaterConstraints = theater.constraints; + final Size boxSize = + theaterConstraints.biggest.isFinite ? theaterConstraints.biggest : theater.size; + deferredChild._doLayoutFrom(this, constraints: BoxConstraints.tight(boxSize)); + } } @override @@ -2522,3 +2602,190 @@ class _RenderLayoutSurrogateProxyBox extends RenderProxyBox { } } } + +class _OverlayChildLayoutBuilder extends AbstractLayoutBuilder { + const _OverlayChildLayoutBuilder({required this.builder}); + + @override + final OverlayChildLayoutBuilder builder; + + @override + RenderAbstractLayoutBuilderMixin createRenderObject( + BuildContext context, + ) => _RenderLayoutBuilder(); + + @override + bool updateShouldRebuild(_OverlayChildLayoutBuilder oldWidget) => oldWidget.builder != builder; +} + +// A RenderBox that: +// - has the same size and paint transform, as its parent and its theater, in +// other words the three RenderBoxes describe the same rect on screen. +// - is a relayout boundary, and gets marked dirty for relayout every frame +// (but only when a frame is already scheduled, and markNeedsLayout does not +// schedule a new frame since it's called in a transient callback). +// - runs a layout callback in performLayout. +// +// Additionally, like RenderDeferredLayoutBox, this RenderBox also uses the Stack +// layout algorithm so developers can use the Positioned widget. +class _RenderLayoutBuilder extends RenderProxyBox + with _RenderTheaterMixin, RenderAbstractLayoutBuilderMixin { + @override + Iterable _childrenInPaintOrder() { + final RenderBox? child = this.child; + return child == null + ? const Iterable.empty() + : Iterable.generate(1, (int i) => child); + } + + @override + Iterable _childrenInHitTestOrder() => _childrenInPaintOrder(); + + @override + _RenderTheater get theater => switch (parent) { + final _RenderDeferredLayoutBox parent => parent.theater, + _ => throw FlutterError('$parent of $this is not a _RenderDeferredLayoutBox'), + }; + + @override + bool get sizedByParent => true; + + @override + void performResize() => size = constraints.biggest; + + @override + void applyPaintTransform(RenderBox child, Matrix4 transform) { + final BoxParentData childParentData = child.parentData! as BoxParentData; + final Offset offset = childParentData.offset; + transform.translate(offset.dx, offset.dy); + } + + @protected + @override + OverlayChildLayoutInfo get layoutInfo => _layoutInfo!; + // The size here is the child size of the regular child in its own parent's coordinates. + OverlayChildLayoutInfo? _layoutInfo; + OverlayChildLayoutInfo _computeNewLayoutInfo() { + final _RenderTheater theater = this.theater; + final _RenderDeferredLayoutBox parent = this.parent! as _RenderDeferredLayoutBox; + final _RenderLayoutSurrogateProxyBox layoutSurrogate = parent._layoutSurrogate; + assert(() { + for ( + RenderObject? node = layoutSurrogate; + node != null && node != theater; + node = node.parent + ) { + if (node is RenderFollowerLayer) { + throw FlutterError.fromParts([ + ErrorSummary( + 'The paint transform cannot be reliably computed because of RenderFollowerLayer(s)', + ), + node.describeForError('The RenderFollowerLayer was'), + ErrorDescription( + 'RenderFollowerLayer establishes its paint transform only after the layout phase.', + ), + ErrorHint( + 'Consider replacing the corresponding CompositedTransformFollower with OverlayPortal.overlayChildLayoutBuilder if possible.', + ), + ]); + } + assert(node.depth > theater.depth); + } + return true; + }()); + assert(layoutSurrogate.hasSize); + assert(layoutSurrogate.child?.hasSize ?? true); + assert(layoutSurrogate.child == null || layoutSurrogate.child!.size == layoutSurrogate.size); + assert(size == theater.size); + assert(layoutSurrogate.child?.getTransformTo(layoutSurrogate).isIdentity() ?? true); + // The paint transform we're about to compute is only useful if this RenderBox + // uses the same coordinates as the theater. + assert(getTransformTo(theater).isIdentity()); + final Size overlayPortalSize = parent._layoutSurrogate.size; + final Matrix4 paintTransform = layoutSurrogate.getTransformTo(theater); + return OverlayChildLayoutInfo._((overlayPortalSize, paintTransform, size)); + } + + int? _callbackId; + @override + void performLayout() { + late OverlayChildLayoutInfo newLayoutInfo; + // The invokeLayoutCallback allows arbitrary access to the sizes of + // RenderBoxes the we know that have finished doing layout. + invokeLayoutCallback((_) => newLayoutInfo = _computeNewLayoutInfo()); + if (newLayoutInfo != _layoutInfo) { + _layoutInfo = newLayoutInfo; + rebuildIfNecessary(); + } + assert(_callbackId == null); + _callbackId ??= SchedulerBinding.instance.scheduleFrameCallback( + _frameCallback, + scheduleNewFrame: false, + ); + layoutChild(child!, constraints); + } + + // This RenderObject is a child of _RenderDeferredLayouts which in turn is a + // child of _RenderTheater. None of them do speculative layout and + // _RenderDeferredLayouts don't participate in _RenderTheater's intrinsics + // calculations. Since the layout callback may mutate the live render tree + // during layout, intrinsic calculations are neither available nor needed. + static const String _speculativeLayoutErrorMessage = + 'This RenderObject should not be reachable in intrinsic dimension calculations.'; + + @override + double computeMinIntrinsicWidth(double height) { + assert(debugCannotComputeDryLayout(reason: _speculativeLayoutErrorMessage)); + return 0.0; + } + + @override + double computeMaxIntrinsicWidth(double height) { + assert(debugCannotComputeDryLayout(reason: _speculativeLayoutErrorMessage)); + return 0.0; + } + + @override + double computeMinIntrinsicHeight(double width) { + assert(debugCannotComputeDryLayout(reason: _speculativeLayoutErrorMessage)); + return 0.0; + } + + @override + double computeMaxIntrinsicHeight(double width) { + assert(debugCannotComputeDryLayout(reason: _speculativeLayoutErrorMessage)); + return 0.0; + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + assert(debugCannotComputeDryLayout(reason: _speculativeLayoutErrorMessage)); + return Size.zero; + } + + @override + double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) { + assert( + debugCannotComputeDryLayout( + reason: + 'Calculating the dry baseline would require running the layout callback ' + 'speculatively, which might mutate the live render object tree.', + ), + ); + return null; + } + + void _frameCallback(Duration _) { + assert(!debugDisposed!); + _callbackId = null; + markNeedsLayout(); + } + + @override + void dispose() { + if (_callbackId case final int callbackId) { + SchedulerBinding.instance.cancelFrameCallbackWithId(callbackId); + } + super.dispose(); + } +} diff --git a/packages/flutter/lib/src/widgets/sliver_layout_builder.dart b/packages/flutter/lib/src/widgets/sliver_layout_builder.dart index da3ec1d677d..dea8f283c6f 100644 --- a/packages/flutter/lib/src/widgets/sliver_layout_builder.dart +++ b/packages/flutter/lib/src/widgets/sliver_layout_builder.dart @@ -29,19 +29,25 @@ class SliverLayoutBuilder extends ConstrainedLayoutBuilder { const SliverLayoutBuilder({super.key, required super.builder}); @override - RenderObject createRenderObject(BuildContext context) => _RenderSliverLayoutBuilder(); + RenderAbstractLayoutBuilderMixin createRenderObject( + BuildContext context, + ) => _RenderSliverLayoutBuilder(); } class _RenderSliverLayoutBuilder extends RenderSliver with RenderObjectWithChildMixin, - RenderConstrainedLayoutBuilder { + RenderAbstractLayoutBuilderMixin { @override double childMainAxisPosition(RenderObject child) { assert(child == this.child); return 0; } + @protected + @override + SliverConstraints get layoutInfo => constraints; + @override void performLayout() { rebuildIfNecessary(); diff --git a/packages/flutter/test/scheduler/scheduler_test.dart b/packages/flutter/test/scheduler/scheduler_test.dart index 1666cfbe370..2244612ccec 100644 --- a/packages/flutter/test/scheduler/scheduler_test.dart +++ b/packages/flutter/test/scheduler/scheduler_test.dart @@ -58,6 +58,9 @@ void main() { tearDown(() { scheduler.additionalHandleBeginFrame = null; scheduler.additionalHandleDrawFrame = null; + PlatformDispatcher.instance + ..onBeginFrame = null + ..onDrawFrame = null; }); test('Tasks are executed in the right order', () { @@ -333,6 +336,29 @@ void main() { expect(isCompleted, true); }); + + test('Can schedule a frame callback with / without scheduling a new frame', () { + scheduler.handleBeginFrame(null); + scheduler.handleDrawFrame(); + bool callbackInvoked = false; + + assert(!scheduler.hasScheduledFrame); + scheduler.scheduleFrameCallback(scheduleNewFrame: false, (_) => callbackInvoked = true); + expect(scheduler.hasScheduledFrame, isFalse); + scheduler.handleBeginFrame(null); + scheduler.handleDrawFrame(); + expect(callbackInvoked, isTrue); + + assert(!scheduler.hasScheduledFrame); + callbackInvoked = false; + scheduler.scheduleFrameCallback((_) => callbackInvoked = true); + expect(scheduler.hasScheduledFrame, isTrue); + scheduler.handleBeginFrame(null); + scheduler.handleDrawFrame(); + expect(callbackInvoked, isTrue); + + assert(!scheduler.hasScheduledFrame); + }); } class DummyTimer implements Timer { diff --git a/packages/flutter/test/widgets/layout_builder_test.dart b/packages/flutter/test/widgets/layout_builder_test.dart index 582743623c7..bdba550b446 100644 --- a/packages/flutter/test/widgets/layout_builder_test.dart +++ b/packages/flutter/test/widgets/layout_builder_test.dart @@ -903,7 +903,7 @@ class _SmartLayoutBuilder extends ConstrainedLayoutBuilder { } @override - RenderObject createRenderObject(BuildContext context) { + _RenderSmartLayoutBuilder createRenderObject(BuildContext context) { return _RenderSmartLayoutBuilder( offsetPercentage: offsetPercentage, onChildWasPainted: onChildWasPainted, @@ -921,7 +921,7 @@ class _SmartLayoutBuilder extends ConstrainedLayoutBuilder { typedef _OnChildWasPaintedCallback = void Function(Offset extraOffset); class _RenderSmartLayoutBuilder extends RenderProxyBox - with RenderConstrainedLayoutBuilder { + with RenderAbstractLayoutBuilderMixin { _RenderSmartLayoutBuilder({required double offsetPercentage, required this.onChildWasPainted}) : _offsetPercentage = offsetPercentage; @@ -961,6 +961,10 @@ class _RenderSmartLayoutBuilder extends RenderProxyBox onChildWasPainted(extraOffset); } } + + @protected + @override + BoxConstraints get layoutInfo => constraints; } class _LayoutSpy extends LeafRenderObjectWidget { diff --git a/packages/flutter/test/widgets/overlay_layout_builder_test.dart b/packages/flutter/test/widgets/overlay_layout_builder_test.dart new file mode 100644 index 00000000000..90a823f787b --- /dev/null +++ b/packages/flutter/test/widgets/overlay_layout_builder_test.dart @@ -0,0 +1,320 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + final OverlayPortalController controller1 = OverlayPortalController(debugLabel: 'controller1'); + setUp(controller1.show); + + testWidgets('Basic test', (WidgetTester tester) async { + late StateSetter setState; + Matrix4 transform = Matrix4.identity(); + late final OverlayEntry overlayEntry; + addTearDown(() { + overlayEntry + ..remove() + ..dispose(); + }); + + late Matrix4 paintTransform; + late Size regularChildSize; + late Rect regularChildRectInTheater; + late Size theaterSize; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: [ + overlayEntry = OverlayEntry( + builder: (BuildContext context) { + return Positioned( + left: 10, + top: 20, + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return Transform( + transform: transform, + // RenderTransform uses size in its applyPaintTransform + // implementation if alignment is set. + alignment: Alignment.topLeft, + child: OverlayPortal.overlayChildLayoutBuilder( + controller: controller1, + overlayChildBuilder: ( + BuildContext context, + OverlayChildLayoutInfo layoutInfo, + ) { + paintTransform = layoutInfo.childPaintTransform; + regularChildSize = layoutInfo.childSize; + regularChildRectInTheater = MatrixUtils.transformRect( + paintTransform, + Offset.zero & layoutInfo.childSize, + ); + theaterSize = layoutInfo.overlaySize; + return const SizedBox(); + }, + child: const SizedBox(width: 40, height: 50), + ), + ); + }, + ), + ); + }, + ), + ], + ), + ), + ); + // Does not schedule a new frame by itself. + expect(tester.binding.hasScheduledFrame, isFalse); + expect(paintTransform, Matrix4.translationValues(10.0, 20.0, 0.0)); + expect(regularChildSize, const Size(40, 50)); + expect(theaterSize, const Size(800, 600)); + expect(regularChildRectInTheater, const Offset(10.0, 20.0) & regularChildSize); + + setState(() => transform = Matrix4.diagonal3Values(2.0, 4.0, 1.0)); + assert(tester.binding.hasScheduledFrame); + await tester.pump(); + + expect(paintTransform, Matrix4.translationValues(10.0, 20.0, 0.0) * transform); + expect(regularChildSize, const Size(40, 50)); + expect(theaterSize, const Size(800, 600)); + expect(regularChildRectInTheater, const Offset(10.0, 20.0) & const Size(80.0, 200.0)); + }); + + testWidgets('child changes size', (WidgetTester tester) async { + late StateSetter setState; + late final OverlayEntry overlayEntry; + addTearDown( + () => + overlayEntry + ..remove() + ..dispose(), + ); + + late Size regularChildSize; + Size childSize = const Size(40, 50); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: [ + overlayEntry = OverlayEntry( + builder: (BuildContext context) { + return Positioned( + left: 10, + top: 20, + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return OverlayPortal.overlayChildLayoutBuilder( + controller: controller1, + overlayChildBuilder: ( + BuildContext context, + OverlayChildLayoutInfo layoutInfo, + ) { + regularChildSize = layoutInfo.childSize; + return const SizedBox(); + }, + child: SizedBox.fromSize(size: childSize), + ); + }, + ), + ); + }, + ), + ], + ), + ), + ); + expect(regularChildSize, childSize); + + setState(() => childSize = const Size(123.0, 321.0)); + + await tester.pump(); + expect(regularChildSize, childSize); + }); + + testWidgets('Positioned works in the builder', (WidgetTester tester) async { + late final OverlayEntry overlayEntry; + addTearDown( + () => + overlayEntry + ..remove() + ..dispose(), + ); + + final GlobalKey key = GlobalKey(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: [ + overlayEntry = OverlayEntry( + builder: (BuildContext context) { + return OverlayPortal.overlayChildLayoutBuilder( + controller: controller1, + overlayChildBuilder: (_, _) { + return Positioned( + left: 123.0, + top: 37.0, + width: 12.0, + height: 23.0, + child: SizedBox(key: key), + ); + }, + child: const SizedBox(width: 10.0, height: 20.0), + ); + }, + ), + ], + ), + ), + ); + + final Rect rect = tester.getRect(find.byKey(key)); + expect(rect, const Rect.fromLTWH(123.0, 37.0, 12.0, 23.0)); + }); + + testWidgets('Rebuilds when the layout info changes', (WidgetTester tester) async { + late StateSetter setState; + Matrix4 transform = Matrix4.identity(); + late final OverlayEntry overlayEntry; + addTearDown( + () => + overlayEntry + ..remove() + ..dispose(), + ); + + late Matrix4 paintTransform; + + Widget buildOverlayChild(BuildContext context, OverlayChildLayoutInfo layoutInfo) { + paintTransform = layoutInfo.childPaintTransform; + return const SizedBox(); + } + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: [ + overlayEntry = OverlayEntry( + builder: (BuildContext context) { + return Positioned( + left: 10, + top: 20, + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return Transform( + transform: transform, + child: OverlayPortal.overlayChildLayoutBuilder( + controller: controller1, + overlayChildBuilder: buildOverlayChild, + child: const SizedBox(width: 40, height: 50), + ), + ); + }, + ), + ); + }, + ), + ], + ), + ), + ); + + expect(paintTransform, Matrix4.translationValues(10.0, 20.0, 0.0)); + setState(() => transform = Matrix4.diagonal3Values(2.0, 4.0, 1.0)); + await tester.pump(); + expect(paintTransform, Matrix4.translationValues(10.0, 20.0, 0.0) * transform); + }); + + testWidgets('Still works if child is null', (WidgetTester tester) async { + late final OverlayEntry overlayEntry; + addTearDown( + () => + overlayEntry + ..remove() + ..dispose(), + ); + + late Size regularChildSize; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: [ + overlayEntry = OverlayEntry( + builder: (BuildContext context) { + return Positioned( + left: 10, + top: 20, + child: OverlayPortal.overlayChildLayoutBuilder( + controller: controller1, + overlayChildBuilder: (BuildContext context, OverlayChildLayoutInfo layoutInfo) { + regularChildSize = layoutInfo.childSize; + return const SizedBox(); + }, + child: null, + ), + ); + }, + ), + ], + ), + ), + ); + expect(regularChildSize, Size.zero); + }); + + testWidgets('Screams if RenderFollower is spotted in path', (WidgetTester tester) async { + late final OverlayEntry overlayEntry; + addTearDown( + () => + overlayEntry + ..remove() + ..dispose(), + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: [ + overlayEntry = OverlayEntry( + builder: (BuildContext context) { + return CompositedTransformFollower( + link: LayerLink(), + child: OverlayPortal.overlayChildLayoutBuilder( + controller: controller1, + overlayChildBuilder: (_, _) => const SizedBox(), + child: null, + ), + ); + }, + ), + ], + ), + ), + phase: EnginePhase.layout, + ); + + expect( + tester.takeException(), + isA().having( + (FlutterError error) => error.message, + 'message', + contains('RenderFollowerLayer'), + ), + ); + }); +} diff --git a/packages/flutter/test/widgets/overlay_portal_test.dart b/packages/flutter/test/widgets/overlay_portal_test.dart index 2c432347629..57f0226c9c0 100644 --- a/packages/flutter/test/widgets/overlay_portal_test.dart +++ b/packages/flutter/test/widgets/overlay_portal_test.dart @@ -26,11 +26,11 @@ class _ManyRelayoutBoundaries extends StatelessWidget { } void rebuildLayoutBuilderSubtree(RenderBox descendant, WidgetTester tester) { - assert(descendant is! RenderConstrainedLayoutBuilder); + assert(descendant is! RenderAbstractLayoutBuilderMixin); RenderObject? node = descendant.parent; while (node != null) { - if (node is! RenderConstrainedLayoutBuilder) { + if (node is! RenderAbstractLayoutBuilderMixin) { node = node.parent; } else { final Element layoutBuilderElement = tester.element( @@ -2793,6 +2793,81 @@ void main() { ); }); }); + + testWidgets( + 'overlay child can compute the paint transform of the regular child relative to the Overlay', + (WidgetTester tester) async { + late StateSetter setState; + EdgeInsets padding = const EdgeInsets.only(left: 10.0); + late Matrix4 computedPaintTransform; + double zOffset = 123.0; + + late final OverlayEntry entry; + addTearDown(() { + entry.remove(); + entry.dispose(); + }); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: [ + entry = OverlayEntry( + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return Transform( + transform: Matrix4.translationValues(0.0, 0.0, zOffset), + child: Padding( + padding: padding, + child: OverlayPortal( + controller: controller1, + overlayChildBuilder: (BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final RenderBox placeholderRenderBox = tester.renderObject( + find.byType(Placeholder), + ); + final RenderBox overlayRenderBox = tester.renderObject( + find.byType(Overlay), + ); + computedPaintTransform = placeholderRenderBox.getTransformTo( + overlayRenderBox, + ); + assert(placeholderRenderBox.hasSize); + return const SizedBox(); + }, + ); + }, + child: const Placeholder(), + ), + ), + ); + }, + ); + }, + ), + ], + ), + ), + ); + + // During the initial layout, the Padding wouldn't have computed its + // child's offset if the overlay child was laid out via treewalk, since + // RenderPadding.performLayout calls child.layout before computing the + // child offset. + expect(computedPaintTransform, Matrix4.translationValues(10.0, 0.0, 123.0)); + + setState(() { + padding = const EdgeInsets.only(top: 20.0); + zOffset = 321.0; + }); + await tester.pump(); + expect(computedPaintTransform, Matrix4.translationValues(0.0, 20.0, 321.0)); + }, + ); } class OverlayStatefulEntry extends OverlayEntry {