Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attach an Element's render object to a designated OverlayEntry #39104

Closed

Conversation

LongCatIsLooong
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong commented Aug 23, 2019

Description

This PR adds a new widget OverlayConduit that manages a regular child and an overlayItem child. When mounted, the overlayItem child has the same behavior as the regular child, except that it attaches its render object to the designated overlayEntry. Thus the overlayItem gets its inherited widgets from the current BuildContext, as the regular child does. This also guarantees that when the entire OverlayConduit subtree unmounts, the removal of listeners and invocation of State.dispose will proceed in an orderly fashion: the deepest elements get removed/disposed of first, preventing #35555 from happening.

Classes in this PR, in a typical setting

overlay

Related Issues

#36220

Tests

I added the following tests:

packages/flutter/test/widgets/overlay_conduit_test.dart

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the CLA.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require Flutter developers to manually update their apps to accommodate your change?

  • Yes, this is a breaking change (Please read Handling breaking changes). Replace this with a link to the e-mail where you asked for input on this proposed change.
  • No, this is not a breaking change.

@fluttergithubbot fluttergithubbot added the framework flutter/packages/flutter repository. See also f: labels. label Aug 23, 2019
@LongCatIsLooong LongCatIsLooong changed the title WIP Attach an Element's render object to a different tree Attach an Element's render object to a designated OverlayEntry Sep 8, 2019
@LongCatIsLooong LongCatIsLooong marked this pull request as ready for review September 8, 2019 08:57
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.

Some comments I noticed during the initial read-through.

@@ -54,8 +54,8 @@ class AbstractNode {
@protected
void redepthChild(AbstractNode child) {
assert(child.owner == owner);
if (child._depth <= _depth) {
child._depth = _depth + 1;
if (child.depth <= depth) {
Copy link
Member

Choose a reason for hiding this comment

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

Why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the default implementation of redepthChild should respect AbstractNode.depth overrides so the minDepth trick in _RenderFollowerProxyBox works

Copy link
Contributor

Choose a reason for hiding this comment

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

That will probably have painful performance implications (moves this from a simple jmp to an indirect jump).

@@ -563,6 +564,9 @@ class _TheatreElement extends RenderObjectElement {
void visitChildren(ElementVisitor visitor) {
if (_onstage != null)
visitor(_onstage);
if (_offstage == null)
Copy link
Member

Choose a reason for hiding this comment

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

When would you call visitChildren while _offstage is still null? Should this be an assert?

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 ran into this issue #37161 when I was poking around. Probably was trying to print the overlay before it was mounted and toString() threw.

Copy link
Contributor

Choose a reason for hiding this comment

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

let's resolve this in a separate PR

final _RenderLeaderProxyBox leaderRenderBox;
}

/// An [OverlayEntry] that allows an [OverlayConduit] to attach its
Copy link
Member

Choose a reason for hiding this comment

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

This probably needs a better doc comment explaining when and for what this should and shouldn't be used.

final GlobalKey _mountPointKey;
_PendingRenderObjectManifest _pendingRenderObject;

@visibleForTesting
Copy link
Member

Choose a reason for hiding this comment

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

Move this annotation to after the comment.

&& element == other.element;
}

// We're not hashing this anyways.
Copy link
Member

Choose a reason for hiding this comment

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

This comment doesn't seem useful.

/// to the designated [ProxyOverlayEntry] [overlayEntry].
const OverlayConduit({
Key key,
this.overlayItem,
Copy link
Member

Choose a reason for hiding this comment

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

Maybe: overlayChild to make it clear that this is the child that goes into the overlay?

assert(overlayItem == null || overlayEntry != null),
super(key: key);

/// A widget whose render object will be attached onto [overlayEntry].
Copy link
Member

Choose a reason for hiding this comment

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

A widget that is build like a regular child, but visually appears in the [overlayEntry].

The widget is build in the build context of this widget, but its render object is attached to the overlayEntry.

}

@immutable
class _OverlaySlot {
Copy link
Member

Choose a reason for hiding this comment

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

If the slot's identity is only defined by the element - can you just use the element as slot?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

_OverlayConduitElement used to have no render object (similar to a ComponentElement) so I added this class to differentiate the overlayChild slot and the child slot. I guess it's safe to get rid of it now.

void forgetChild(Element child) {
if (child == _child)
child = null;
assert(child == _overlayItem);
Copy link
Member

Choose a reason for hiding this comment

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

This is strange: The only child we can forget is the overlayItem? What about the child child? Can those not be different?

Copy link
Member

Choose a reason for hiding this comment

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

Or is the if missing a return maybe? (If so, we should add a test that fails without it)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh it's missing a return. Thanks for catching that!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

child = null; didn't know it was possible 👀

// If mountPoint is already built.
assert(mountPoint.renderObject != null);
mountPoint.renderObject.leader = renderObject;
mountPoint.insertChildRenderObject(child, slot);
Copy link
Member

Choose a reason for hiding this comment

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

Does this one have multiple slots? Can slot just be null?

@Hixie
Copy link
Contributor

Hixie commented Oct 21, 2019

@LongCatIsLooong Any chance you can merge this to tip of tree? I tried checking out the branch but I got a lot of merge conflicts.

@Hixie
Copy link
Contributor

Hixie commented Oct 22, 2019

So I tried this out yesterday and by and large it seems to work as expected (which is impressive! Wow!). However, I think we need to separate out the conduit API from Overlay. Having them be tied too closely leads to all kinds of problems, for example you can't use Positioned in the overlayChild.

The other weird thing about this API is that if you have multiple children you want to move to elsewhere in the tree you need one widget for each one. That took me a while to understand. Once I got the hang of it I was able to work with that easily enough.

Here's the code I wrote to experiment with this: http://junkyard.damowmow.com/606

I think I need to do more experiments, but I'd like to wait to have a version that isn't tied to the Overlay before I do that.

@LongCatIsLooong LongCatIsLooong force-pushed the overlay-render-object branch 4 times, most recently from f36d8b6 to 6f2d077 Compare October 27, 2019 07:36
@Piinks Piinks added a: animation Animation APIs f: routes Navigator, Router, and related APIs. a: text input Entering text in a text field or keyboard related problems a: typography Text rendering, possibly libtxt labels Nov 11, 2019

import 'framework.dart';

// When an OverlayConduit tries to mount its overlayItem onto its overlayEntry's
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like the comments need updating

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@LongCatIsLooong LongCatIsLooong force-pushed the overlay-render-object branch 2 times, most recently from 2cef766 to 0db2ab9 Compare December 17, 2019 06:27
@Ahmadre
Copy link

Ahmadre commented Dec 30, 2019

any updates? Can we merge this?

@Ahmadre
Copy link

Ahmadre commented Dec 30, 2019

Oh ok, I see it's just in progress :) gtk

_RenderLeaderProxyBox([RenderBox child]) : super(child);

_RenderFollowerProxyBox __follower;
_RenderFollowerProxyBox get _follower => __follower;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: since the class is private, the member can be public. this lets you avoid the double-underscore thing.

@Hixie
Copy link
Contributor

Hixie commented Jan 7, 2020

I like this new approach. I need to try playing with it.

@Hixie
Copy link
Contributor

Hixie commented Jan 8, 2020

@LongCatIsLooong points out for (3) that when the Leader is pushed down, its nodes are (temporarily) removed from the tree which also removes the children from the Follower, so it actually reduces to case (1).

So really all we need to do is make sizedByParent prevent the layout() call from laying out the children immediately.

@goderbauer
Copy link
Member

Looking at the test failures, this probably requires a rebase to make cirrus happy.

@LongCatIsLooong
Copy link
Contributor Author

@Hixie @goderbauer I tried out the "throw sizedByParent children back to the dirty list after performResize" trick in ec0680107f76148e9e6f7eebb437c19f12e91f86. The new tests are passing but some existing tests started to fail.

It seems some render objects need not only the size of their child but also the child's layout, e.g. render objects that call their child's getDistanceToActualBaseline. In this case we can't defer the child's performLayout even if it's sized by its parent.
Does it make sense to add a flag to RenderObject that indicates whether the parent is allowed to use the child's layout, or add it to RenderObject.layout (so it becomes something like void layout(Constraints constraints, { bool parentUsesSize = false, bool parentUsesLayout = false }))?
A quick search for "implements Render" in the registered tests yielded nothing.

@LongCatIsLooong
Copy link
Contributor Author

LongCatIsLooong commented Feb 7, 2020

If both subtrees are dirty and the Conduit branch lays out first, does making the anchor sizedByParent causes the layout of the remote branch to happen twice (triggered in both subtrees)? I guess we want to make the Conduit node deeper instead?
The current road blocker is when a ConduitAnchor is deferred to the next layout iteration, its descendant render objects that are also in the dirty list will be laid out more than once. I'll see if we can defer these nodes to the next iteration too.

@LongCatIsLooong LongCatIsLooong changed the title Attach an Element's render object to a designated OverlayEntry [WIP] Attach an Element's render object to a designated OverlayEntry Feb 8, 2020
@goderbauer
Copy link
Member

@LongCatIsLooong Do you think there still is a path forward for this PR? Are you still working on it?

@LongCatIsLooong
Copy link
Contributor Author

Yes I'm still (not very actively) working on this. I think it may still be doable, just need to find an approach that requires less crazy framework changes.

@LongCatIsLooong
Copy link
Contributor Author

LongCatIsLooong commented Apr 20, 2020

@Hixie @goderbauer all the tests seem to be passing now. Could you take a look? I added a shouldDeferLayout getter and a markDepedentNeedsLayout(RenderObject) method to RenderObject.

@LongCatIsLooong LongCatIsLooong changed the title [WIP] Attach an Element's render object to a designated OverlayEntry Attach an Element's render object to a designated OverlayEntry Apr 28, 2020
Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

For the depthbarrier, if i understand correctly, it is trying to prevent the attached children under conduit anchor to be laid out before the anchor. If that is the intention, have you checked out this https://github.com/rrousselGit/flutter_portal/blob/rework/lib/src/portal.dart?

This is @rrousselGit attempt to deal with this problem and i think it worth considering. The goal is the conduit widget is not responsible for laying out or paint its remote child. It merely attach its remote child to the anchor key. When the conduit anchor layout, it pick up the anchor key and see which children it has and proceed to layout and paint them. The entire discussion can be found in #50961

Besides the comment, I have a general question, It seems like the attempt here is to wrapped the overlay with conduit and conduitanchor to provide declarative overlay api. The portal is trying to achieve the same thing except it replace the overlay with an entire different system Portal. Why do we prefer one over the other?

///
/// The [dependent] must be [attached], has the same [PipelineOwner] as this
/// [RenderObject], and its [shouldDeferLayout] and [sizedByParent] must be true.
/// Additionally To prevent introducing circular dependencies, [dependent]'s
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Additionally To prevent introducing circular dependencies, [dependent]'s
/// Additionally to prevent introducing circular dependencies, [dependent]'s

class _RenderConduit extends RenderProxyBox {
_RenderConduit([RenderBox child]) : super(child);

_RenderConduitAnchor _follower;
Copy link
Contributor

Choose a reason for hiding this comment

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

I was confused by the name. If i read it correctly, shouldn't the Conduit be follower and ConduitAnchor be the leader because conduit attach its child the conduitanchor?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The anchor is named follower in the sense that it depends on its conduit. The anchor changes if its conduit changes, but not the other way around. I guess dependent/dependency sounds good here too.

Copy link
Contributor

Choose a reason for hiding this comment

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

The problem with naming the anchor follower is that you then have one follower with multiple leaders which is counterintuitive. Usually there should only be one leader with multiple followers. The dependent/dependency sounds good

@rrousselGit
Copy link
Contributor

The portal is trying to achieve the same thing except it replace the overlay with an entire different system Portal. Why do we prefer one over the other?

I can answer that question:
Because portal was implemented outside of Flutter. So I was unable to modify Overlay, and instead made a fork.

Although if it were to be merged into Flutter, it could still be valuable to separate them, to help the migration.
Kind of like Router vs Navigator

@rrousselGit
Copy link
Contributor

rrousselGit commented May 4, 2020

The goal is the conduit widget is not responsible for laying out or paint its remote child.

In the rework branch, the mechanism is slightly different.

The conduit does the layout of the overlay entry (where the constraints are obtained with an object equivalent to LayerLink).
And the Overlay does the painting of the entries
This way it does not rely on having a specific layout order to work (so no situation where the widget may build twice like @Hixie mentioned).

Although I'm not sure if this has undesired side-effects

@LongCatIsLooong
Copy link
Contributor Author

LongCatIsLooong commented May 6, 2020

@rrousselGit Thanks for the explanation! I think the current implementation in the rework branch will probably break RenderObject.getTransformTo (and maybe some other things that rely on the hierarchy of the render object tree), nonetheless it looks really promising! It makes more sense to me to paint somewhere else than to move render objects around. 👍

@rrousselGit
Copy link
Contributor

@rrousselGit Thanks for the explanation! I think the current implementation in the rework branch will probably break RenderObject.getTransformTo (and maybe some other things that rely on the hierarchy of the render object tree), nonetheless it looks really promising! It makes more sense to me to paint somewhere else than to move render objects around.

It did, but I managed to fix it (it's in the branch)

My biggest worry was about RepaintBoundary & co, wondering if there isn't an edge case where the UI doesn't refresh or something

Haven't found anything so far though.

@rrousselGit
Copy link
Contributor

rrousselGit commented Jun 19, 2020 via email

@LongCatIsLooong
Copy link
Contributor Author

@rrousselGit no I'm just trying to reduce the number of open PRs since I'm currently not working on this.

@rrousselGit
Copy link
Contributor

What remains to be done?

Between Hero/package:animation issues, state restoration, and provider having difficulties, I think it's quite important and may continue it myself.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jul 30, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
a: animation Animation APIs a: text input Entering text in a text field or keyboard related problems a: typography Text rendering, possibly libtxt f: routes Navigator, Router, and related APIs. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants