Skip to content

Conversation

@LongCatIsLooong
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong commented Jun 3, 2022

Inspired by the flutter_portal package.

The flutter_portal package is created by @rrousselGit and is currently maintained by @fzyzcjy

This PR resolves many of the issues raised in #36220.

What the Render Tree Looks like:

Layout Order

_RenderDeferredLayoutBox needs to do layout after _RenderLayoutSurrogateProxyBox, but should also update its layout when _RenderTheatre gives it a set of different constraints. Additionally, RenderObjects below a _RenderDeferredLayoutBox should only perform layout after the _RenderDeferredLayoutBox is updated.

So the layout order has to be _RenderTheatre -> _RenderLayoutSurrogateProxyBox -> _RenderDeferredLayoutBox -> overlayChild, otherwise something may have to do layout twice.

  • _RenderTheatre is the common ancestor of the 3 other RenderObjects so it will be laid out first. It's ok to re-dirty it since it's sizedByParent, and all that matters to the descendants is the constraints, which won't change even when re-dirtied.
  • _RenderLayoutSurrogateProxyBox -> _RenderDeferredLayoutBox: _RenderDeferredLayoutBox has a greater depth than _RenderLayoutSurrogateProxyBox and it can't be tree walked from _RenderTheatre.
  • _RenderLayoutSurrogateProxyBox -> overlayChild: because _RenderDeferredLayoutBox -> overlayChild. When _RenderDeferredLayoutBox is not dirty, overlayChild is guaranteed to have a greater depth than _RenderLayoutSurrogateProxyBox.
  • _RenderDeferredLayoutBox -> overlayChild: overlayChild can only be tree walked from _RenderDeferredLayoutBox and has a greater depth than _RenderDeferredLayoutBox.

However since _RenderDeferredLayoutBox actually depends on both _RenderLayoutSurrogateProxyBox and _RenderTheatre, it can be dirtied by _RenderTheatre when _RenderLayoutSurrogateProxyBox is clean. When that happens _RenderTheatre needs to call _RenderDeferredLayoutBox.markNeedsLayout in an invokeLayoutCallback to merge the node back to the dirty list in case overlayChild is dirty too, so overlayChild doesn't perform layout before _RenderDeferredLayoutBox.

Why placing the RenderObject under _RenderTheatre instead of _RenderLayoutSurrogateProxyBox

It takes some extra lines of code to manage the render tree properly, and _RenderLayoutSurrogateProxyBox has to redepth the overlayChild branch, but:

  1. It generates the same render tree as the OverlayEntry API would and that could make it easier for people to migrate from the OverlayEntry API.
  2. If overlayChild needs to repaint it doesn't have to mark _RenderLayoutSurrogateProxyBox as needing repaint too (we have markParentNeedsLayout but not markParentNeedsPaint). overlayChild paints on _RenderTheatre's PaintContext so there's little point in marking _RenderLayoutSurrogateProxyBox dirty. Also during compositing bits update, it could set _needsCompositing to true on some nodes that actually don't need compositing.

TODOs to be addressed in future pull requests

  1. Currently when an OverlayPortal widget reparents, the overlay child will be moved to the top of the Overlay (in terms of the z-index, for painting and hit-testing), and that's pretty strange for a declarative API. OverlayInfo currently knows which OverlayEntry an OverlayPortal belongs to (for nested OverlayPortal it shouldn't be difficult to get the closest ancestor OverlayPortal too), so it shouldn't be difficult to add additional information to the parent data to specify the desired z-index (in relation to the parent OverlayEntry or PortalEntry).

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@flutter-dashboard flutter-dashboard bot added d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos documentation f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. c: contributor-productivity Team-specific productivity, code health, technical debt. labels Jun 3, 2022
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this?

Copy link
Contributor Author

@LongCatIsLooong LongCatIsLooong Jun 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's Overlay.of but uses an InheirtedWidget (Overlay.of uses context.findAncestorStateOfType<OverlayState>() because we didn't have a declarative API that requires the consumer widget to be notified when the overlay changes).

Also the widget that provides OverlayInfo lives under an OverlayEntry so it knows which OverlayEntry it is from. We can add more (private) information to OverlayInfo when needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why OverlayPortal can't do that itself? I'd expect OverlayInfo.of(context) to use the OverlayPortal's Element as context

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not always I think? There're cases you want to put the child on a different Overlay (that's why Overlay.of takes a bool rootOverlay = false I guess. Unfortunately it will be a bit expensive for OverlayInfo.of to get the root overlay and updates its dependents when it changes)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the time it should I think
I'd at least ewpect this parameter to be optional

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, I don't disagree with the value of this parameter.
My only argument is about making it optional and default to obtaining the closest Overlay.

There are definitely use-cases for this parameter. But I don't see value in having to pass OverlayInfo.of(contewt)

To me, that's as if Text's style was required and expected users to do Text('...', style: DefaultTextStyle.of(context))

Copy link
Contributor Author

@LongCatIsLooong LongCatIsLooong Jun 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use null for default then unfortunately it can have 2 meanings: either the API user wants to use the default, or we failed to get the OverlayInfo from the given BuildContext the user specified at runtime. You can probably workaround this by creating some const value to represent the default (the closest OverlayInfo), but I think it's better to keep the API simple and stupid in this case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't expect OverlayInfo.of to return null if there's no overlay anyway. I'd expect an exception
So this wouldn't have 2 meanings

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah usually we have 2 versions for this kind of things, maybeOf and of. I'll rename Overlay.of to Overlay.maybeOf to follow the naming convention.

But I'm a bit hesitant to only expose the version that throws, especially since this is a query API. It will be a bit awkward for people to do something that could otherwise be really simple to achieve: OverlayInfo.maybeOf(outerContext) ?? Overlay.maybeOf(innerContext).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By popular demand I'll rename the current default constructor and introduce a new default constructor that doesn't take an OverlayInfo?.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will "mouse" region behave well for mobile phones? If not, maybe we can improve this example since flutter targets on mobiles as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interactive example in the documentation and will be shown in a browser so I think in most cases developers will have a mouse connected while checking out the example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But yeah you're right I'll change it to a button so it's more accessible.

Comment on lines 1581 to 1590
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I am a bit confused. If assert(false), how can the lines after it be executed? (the result = false etc). Especially considering all these are wrapped in assert((){ }) such that the whole block will only exist in debug build.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change that to throw a FlutterError then.

Comment on lines 1073 to 1080
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 are these temporary code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Hixie currently we allow a node to mark itself dirty if there's no ancestors of it calling performLayout. I thought that could cause clean nodes to be re-dirtied? Changing it to setting result to false here doesn't seem to break any tests so I wonder why that was allowed? Is that for global key reparenting between LayoutBuilders?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not recall, but yeah, looking at global key reparenting and layout builders (and maybe both together) is definitely where I would start when trying to understand this. cc @goderbauer might have a better idea, he's more familiar with this code these days.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not always I think? There're cases you want to put the child on a different Overlay (that's why Overlay.of takes a bool rootOverlay = false I guess. Unfortunately it will be a bit expensive for OverlayInfo.of to get the root overlay and updates its dependents when it changes)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interactive example in the documentation and will be shown in a browser so I think in most cases developers will have a mouse connected while checking out the example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But yeah you're right I'll change it to a button so it's more accessible.

Comment on lines 1581 to 1590
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change that to throw a FlutterError then.

@LongCatIsLooong
Copy link
Contributor Author

Also on a different note, @rrousselGit @fzyzcjy how would like to receive the due credit? I can add a code comment before the OverlayPortal class and say "this is Inspired by the flutter_portal package"? If that works for you, do you want your github handles added to the comment as well?

@rrousselGit
Copy link
Contributor

"this is Inspired by the flutter_portal package"? If that works for you, do you want your github handles added to the comment as well?

That's fine by me :)

@fzyzcjy
Copy link
Contributor

fzyzcjy commented Jun 7, 2022

"this is Inspired by the flutter_portal package"? If that works for you, do you want your github handles added to the comment as well?

Also fine for me :)

@LongCatIsLooong LongCatIsLooong marked this pull request as ready for review June 8, 2022 00:47
@LongCatIsLooong LongCatIsLooong changed the title [WIP] OverlayEntry alternative OverlayPortal Jun 8, 2022
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moving changes to object.dart into its own PR would be good, given the sensitivity of such changes (e.g. you probably want to try to get low-level changes like this rolled into google3 before doing anything that relies on them)

@Hixie
Copy link
Contributor

Hixie commented Jun 8, 2022

This seems almost guaranteed to cause some unexpected interaction, but I admit I cannot think what it might be...

We should have tests for at least all of the following:

  • GlobalKey reparenting between a regular overlay entry and a portal entry, both forwards and backwards, and in both orders (i.e. four tests).
  • GlobalKey reparenting between a two portal entries, both forwards and backwards, and in both orders.
  • GlobalKey reparenting between a portal entry and the regular child of the surrogate, in both directions and both orders.
  • Something that only causes the portal to rebuild, the portal child to rebuild, the proxy to rebuild, the proxy's regular child to rebuild.
  • Every test again, but with a layout builder between the overlay and the proxy.
  • Nested overlays with nested portals (overlays inside portals, inside proxies, etc).

@nilsreichardt
Copy link
Contributor

cc: @creativecreatorormaybenot

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given there are so few, do you think it makes sense to credit the other contributors as well? I am not sure myself because looking at the implementation, I do realize that it does differ significantly from the code we wrote.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah my apologies. Yeah I can pull the list from the bottom of this page: https://pub.dev/packages/flutter_portal. Alternatively there's an AUTHORS file, do you prefer to be added there instead (also @rrousselGit @fzyzcjy).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not really have a preference there 👍

@LongCatIsLooong
Copy link
Contributor Author

Started working on adding more test, @Hixie by proxy you mean the PortalOverlay widget right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like a really inefficient way of implementing:

if (_child != null) visitor(_child);
if (_overlayChild != null) visitor(_overlayChild);

Am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't want to create 2 local variables just for promoting them to non-null types I guess.

@Hixie
Copy link
Contributor

Hixie commented Jun 10, 2022

Started working on adding more test, @Hixie by proxy you mean the PortalOverlay widget right?

I meant the addDeferredChild API, but I guess that's the same thing since it's private.

(It'd be interesting to see if there was a way to implement this so that Overlay and _Theatre itself was agnostic about all this, so that other people could create their own interesting variants.)

@LongCatIsLooong
Copy link
Contributor Author

(It'd be interesting to see if there was a way to implement this so that Overlay and _Theatre itself was agnostic about all this, so that other people could create their own interesting variants.)

Something like #39104? The addDeferredChild thing is only safe on a render parent whose children are independently sized, and independently positioned, in case it's called inside of a LayoutBuilder, so it can adopt render children during layout and finish everything in one frame. So I guess the only thing that qualifies is an Overlay/_RenderTheatre?

@Hixie
Copy link
Contributor

Hixie commented Jul 7, 2022

@LongCatIsLooong I tried checking this out to play with it but it had a lot of conflicts (and many commits so it was hard to figure out how to resolve them) -- any chance you could merge this to tip of tree?

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in the last commit LGTM

@LongCatIsLooong LongCatIsLooong added the autosubmit Merge PR when tree becomes green via auto submit App label Mar 2, 2023
@auto-submit auto-submit bot merged commit e7ab3b0 into flutter:master Mar 2, 2023
@LongCatIsLooong LongCatIsLooong deleted the overlay-api branch March 2, 2023 17:34
@LongCatIsLooong
Copy link
Contributor Author

Thanks everyone for your input!

@fzyzcjy
Copy link
Contributor

fzyzcjy commented Mar 2, 2023

What a long journey! Congratulations 🎉

@creativecreatorormaybenot
Copy link
Contributor

🥳

engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 3, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 3, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request May 10, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request May 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App c: contributor-productivity Team-specific productivity, code health, technical debt. d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.