Skip to content

Conversation

@LongCatIsLooong
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong commented Feb 19, 2025

This change makes overlayChild unreachable via regular layout treewalk, so the paint transform from OverlayPortal.child to the target overlay can be reliably computed during layout (assuming the paint transform of all nodes in the path are known during layout, see the comments below).

This is part of the changes that will eventually (hopefully) enable an Overlay API that allows an accessory view to change its size and location based on the main view's size and location in the Overlay (similar to CompositedTransform{Target, Follower}) .

Notable use cases of such an API:

  • Autocomplete, where the options view is anchored to the input field, but its size is also limited by the gap between the input field and the edge of the Overlay (and currently computing that has an 1-frame delay).
  • Various text field context menus / tooltips that situate themselves at the bottom of the input field if the top of the input field is too close to the app bar / top edge of the screen.
  • Slider value indicators that change shape to avoid running into screen edges.

This PR does not enable the API by itself, since the accessory view would need to be marked as needing relayout when the main view's paint transform within the Overlay changes. See <PR #TBD> for the implementation that marks the overlay child dirty when the paint transform changes.

Differences between the new API and CompositedTransform{Target, Follower}

The new API's use cases largely overlap that of CompositedTransform{Target, Follower}, but it additionally:

  • allows the layout of the overlay child to depend on the paint transform of the regular overlay child, while CompositedTransformFollower can only follow.
  • does not have the paint order restriction CompositedTransformFollower has (that the CompositedTransformTarget must come earlier in the paint order than this CompositedTransformFollower).

On the other hand:

  • the new API only works in an Overlay. In rare cases the parent RenderObject may need the size of the follower (can't really think of one though).
  • the new API makes the assumption that the desired paint transform is up-to-date during the layout phase, which is not true if there's a RenderFollowerLayer in the path, as its paint transform is only updated in addToScene, way after the layout process is done. So it's a bit leaky since whether there's a RenderFollowerLayer is an implementation detail.

I'll try to add asserts to catch the incompatibility. If the new API works then i'd expect it to significantly reduce the use of CompositedTransformFollower.

Part 2 of this PR: LongCatIsLooong/flutter@OverlayPortal-always-add-to-dirty-list...LongCatIsLooong:flutter:OverlayPortal-paint-transform-builder

Pre-launch Checklist

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

Such that nobody `implements` it. Currently it has private
APIs used by the `RenderObject` class (e.g., the dirty lists).
If someone were to implement a `PipelineOwner` outside of
object.dart, they will get a runtime crash since they won't be
able to provide those private APIs and RenderObjects actually
expect those APIs to exist.
This allows `overlyChild` to inspect the layout of the regular
child (for example, computing the size and the paint transform from the
Overlay to the regular child in `overlayChild`'s performLayout method).
@github-actions github-actions bot added the framework flutter/packages/flutter repository. See also f: labels. label Feb 19, 2025
@knopp
Copy link
Member

knopp commented Feb 24, 2025

In case it is useful to someone: It is already possible to have overlay widget that tracks the anchor and can adjust size / position depending on available space without the 1 frame delay.

Here is it in action. You can see it in action by opening the overlay, making the window small and scrolling. In the example size is not adjusted but the constrains are calculated dynamically taking anchor location into action so it's possible.

EDIT: Actually, the size in example is adjusted dynamically so the popover always fits the bounds.

And here is the implementation. It relies on persistent frame callback to ensure the layout always up to date. but due to layout boundaries the overhead should be negligible.

@LongCatIsLooong
Copy link
Contributor Author

LongCatIsLooong commented Feb 24, 2025

In case it is useful to someone: It is already possible to have overlay widget that tracks the anchor and can adjust size / position depending on available space without the 1 frame delay.

Here is it in action. You can see it in action by opening the overlay, making the window small and scrolling. In the example size is not adjusted but the constrains are calculated dynamically taking anchor location into action so it's possible.

EDIT: Actually, the size in example is adjusted dynamically so the popover always fits the bounds.

And here is the implementation. It relies on persistent frame callback to ensure the layout always up to date. but due to layout boundaries the overhead should be negligible.

@knopp This is very interesting, thanks! Quick question after looking at the implementation: it looks like the implementation maintains its own persistent callback queue, which will be drained after RenderBinding.drawFrame (where PipelineOwner flushes layout/paint etc). The new persistent callbacks (_onFrame) added to the queue seem to only dirty render objects. So those dirty nodes will only redo layout in the next frame, since RenderBinding.drawFrame is called before _onFrame?

I couldn't find a way to schedule those markNeedsLayout calls before drawFrame so I decided to use transient callbacks and added a flag to prevent the transient callback from scheduling a new frame in #164034

@knopp
Copy link
Member

knopp commented Feb 25, 2025

It calls markNeedsLayout after the frame, but it doesn't schedule new frame. That basically means the layout is always dirty and will be computed on each frame, while the popover is visible, no matter what.

The second reason why this works is that the _RenderDeferredLayoutBox depth is always higher than the anchor, and because it's a relayout boundary we can be sure that the layout is always performed after the anchor layout is already done

(it's been a while since a wrote this so maybe I'm misremembering something, but this should be it).

@LongCatIsLooong LongCatIsLooong marked this pull request as ready for review March 3, 2025 18:46
@goderbauer
Copy link
Member

In rare cases the parent RenderObject may need the size of the follower (can't really think of one though).

To check my own understanding: CompositedTransform{Target, Follower} also doesn't support this, right?

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.

Overall this looks reasonable to me.

/// child undergoes layout. Otherwise, the child can change its layout
/// information without informing this render object.
///
/// It is recommended to establish / update the paint transform of this render
Copy link
Member

Choose a reason for hiding this comment

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

I assume this means you want applyPaintTransform to return the correct answer after layout? Maybe link to that method from here so people have context on what the "paint transform" is?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe also have it link to getTransformTo for even more 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.

Rephrased the paragraph. Note to myself to change the backticks to square brackets in PR2 for the OverlayPortal.overlayChildLayoutBuilder reference.

// 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
Copy link
Member

Choose a reason for hiding this comment

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

For my own understanding (and worthwhile to document here): the after is guaranteed because this RO has a higher depth than the theater and surrogate?

}

@override
void layout(Constraints constraints, {bool parentUsesSize = false}) {
Copy link
Member

Choose a reason for hiding this comment

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

Is it okay that we completely ignore the parentUsesSize here? Maybe leave a comment on why? And should we also assert that it actually isn't set by the parent?

@LongCatIsLooong
Copy link
Contributor Author

In rare cases the parent RenderObject may need the size of the follower (can't really think of one though).

To check my own understanding: CompositedTransform{Target, Follower} also doesn't support this, right?

I think I was thinking about the following case:

Flex(
  children: [ A, B,  CompositedTransformFollower_C, D]
)

where the parent render box would still reserve space for the transform follow (C), and at the same time C's offset is determined by something else (its Target). I don't know if there are practical use cases

github-merge-queue bot pushed a commit that referenced this pull request Mar 14, 2025
This API allows the widget tree of an `OverlayPortal.overlayChild` to
depend on various layout information (e.g. the incoming constraints, or
the size of a RenderObject) from another child subtree of `Overlay` that
does layout before the `overlayChild`.

Most RenderObject subclasses can only access its child's or children's
layout info, but not the layout info of its other descendants because of
the "relayout boundary" optimization. Such locality makes the layout
dependencies easier to reason about but it also makes it difficult to
implement certain common UI patterns (see the examples in the
description of the [previous
PR](#163575))

The API is currently only available on `OverlayPortal` as it is the only
Overlay API (AFAIK) that guarantees every render object in a "path"
within the render tree has finished doing layout.

TODO: polish the API docs and code comments
TODO: more tests?

TODO: markNeedsLayout should not imply markNeedsPaint in this case (or
in layout builders in general).

part1: #163575
diff this ... part1
LongCatIsLooong/flutter@OverlayPortal-always-add-to-dirty-list...LongCatIsLooong:flutter:OverlayPortal-paint-transform-builder

## 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].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

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

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
romanejaquez pushed a commit to romanejaquez/flutter that referenced this pull request Aug 14, 2025
This API allows the widget tree of an `OverlayPortal.overlayChild` to
depend on various layout information (e.g. the incoming constraints, or
the size of a RenderObject) from another child subtree of `Overlay` that
does layout before the `overlayChild`.

Most RenderObject subclasses can only access its child's or children's
layout info, but not the layout info of its other descendants because of
the "relayout boundary" optimization. Such locality makes the layout
dependencies easier to reason about but it also makes it difficult to
implement certain common UI patterns (see the examples in the
description of the [previous
PR](flutter#163575))

The API is currently only available on `OverlayPortal` as it is the only
Overlay API (AFAIK) that guarantees every render object in a "path"
within the render tree has finished doing layout.

TODO: polish the API docs and code comments
TODO: more tests?

TODO: markNeedsLayout should not imply markNeedsPaint in this case (or
in layout builders in general).

part1: flutter#163575
diff this ... part1
LongCatIsLooong/flutter@OverlayPortal-always-add-to-dirty-list...LongCatIsLooong:flutter:OverlayPortal-paint-transform-builder

## 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].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

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

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants