-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Add RenderObject.computeTransformToRenderObject
#130192
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
Add RenderObject.computeTransformToRenderObject
#130192
Conversation
| return transform; | ||
| } | ||
|
|
||
| /// Returns the paint transform that allows this `RenderObject` to perform |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// Returns the paint transform that allows this `RenderObject` to perform | |
| /// Returns the paint transform that allows this [RenderObject] to perform |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(and elsewhere where the name of a link-able class is used)
| /// * [getTransformTo] which does the same, but only searches up the tree. | ||
| /// * [paintsChild] which this method uses to determine whether | ||
| /// a `RenderObject` is in an offscreen subtree. | ||
| Matrix4? computeTransformToRenderObject(RenderObject toRenderObject) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For my own understanding: Why does this need new API instead of just making getTransformTo behave this way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was a bit concerned that someone would actually depend on the current behavior of getTransformTo but since it just asserts I guess that's unlikely. I'll change the behavior of getTransformTo instead.
| return null; | ||
| } | ||
|
|
||
| static bool _computePaintTranfrom(RenderObject fromRenderObject, Matrix4 fromTransform, RenderObject toRenderObject, Matrix4 toTransform) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe document what the boolean return value means. It's not clear from the method name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, documenting some of the parameters would also be helpful, especially the differences between the two transforms that this produces.
There was a problem hiding this comment.
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 probably good to document): fromTransform is the transformation from the fromRenderObject to a potential common ancestor and toTransform is the transformation from that common ancestor to the toRenderObject - then if we multiply these two we end up with the transformation to allow the fromRenderObject to paint in the toRenderObject?
| if (identical(fromRenderObject, toRenderObject)) { | ||
| return true; | ||
| } | ||
| final bool fromLowerThanTo = fromRenderObject.depth >= toRenderObject.depth; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uber-nit: variable name says lowerThan - logic allows for equal. Is the equal important here?
| /// See also: | ||
| /// | ||
| /// * [getTransformTo] which does the same, but only searches up the tree. | ||
| /// * [paintsChild] which this method uses to determine whether |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does getTransformTo consider this or is it another difference?
| return null; | ||
| } | ||
|
|
||
| static bool _computePaintTranfrom(RenderObject fromRenderObject, Matrix4 fromTransform, RenderObject toRenderObject, Matrix4 toTransform) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo - I think?
| static bool _computePaintTranfrom(RenderObject fromRenderObject, Matrix4 fromTransform, RenderObject toRenderObject, Matrix4 toTransform) { | |
| static bool _computePaintTransform(RenderObject fromRenderObject, Matrix4 fromTransform, RenderObject toRenderObject, Matrix4 toTransform) { |
|
|
||
| // Computes the paint transform from this to toRenderObject and applies it on | ||
| // `transform`. | ||
| Matrix4 _computePaintTransform(RenderObject toRenderObject, Matrix4 transform) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that there is a static function basically with the same name is very confusing since they do different things. Should this maybe be _computePaintTransformToAncestor or something like that? and toRenderObject could be renamed ancestor.
| return _computePaintTransform(ancestor, identity); | ||
| } else { | ||
| final RenderObject? parent = this.parent; | ||
| return parent != null ? _computePaintTransformToGlobalCoordinates(parent, identity) : identity; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logically it seems odd that calculating the path all the way to the root (ancestor == null) requires a completely different code path than calculating it to a given ancestor. If anything, I would have at least expected that the shape of these methods is the same, just with a different stop condition...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think when ancestor is null the expected behavior of the method is to find the closest RenderView (since RenderView.applyPaintTransform applies the device's pixel ratio)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have expected that it just walks all the way to root node of the pipeline owner without making any assumptions what that root node is. As documented, if people want to convert to physical pixels, they'd have to do that manually by applying the transform of the RenderView.
| /// | ||
| /// * [computeTransformToRenderObject] which does the same, but the target | ||
| /// `RenderObject` does not have to be an ancestor. | ||
| Matrix4 getTransformTo(RenderObject? ancestor) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For my own understanding: Why refactor this method as well? What is the benefit? Is it faster? (At first I thought it was to make code sharable between the two methods, but that doesn't seem to be the case?)
FWIW, for my personal taste, the old implementation was more self-documenting and easier to follow along what it does. It was also only half as long. The new implementation is less straight-forward and introduces separate code paths depending on the argument...
| expect(renderObject1.computeTransformToRenderObject(renderObject1), equals(Matrix4.identity())); | ||
| expect(renderObject11.computeTransformToRenderObject(renderObject11), equals(Matrix4.identity())); | ||
| expect(renderObject12.computeTransformToRenderObject(renderObject12), equals(Matrix4.identity())); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a test that actually compute something meaningful to make sure the implementation actually does what it promises? I could satisfy all these tests by "accidentally" replacing the implementation of computeTransformToRenderObject with:
Matrix4 computeTransformToRenderObject(...) => Matrix4.identity();| renderers[index].applyPaintTransform(renderers[index - 1], transform); | ||
| final Matrix4 inverseTransform = Matrix4.identity(); | ||
| if (_computePaintTranfrom(this, transform, toRenderObject, inverseTransform) && inverseTransform.invert() != 0) { | ||
| return inverseTransform..multiply(transform); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mathematically, this reads confusing. Doesn't multiplying the inverse matrix with the original matrix always give you the identity matrix by definition? So why not shortcut all this and just return Matrix4.identity()? 😄
|
Updated the code and stuck to the |
| @@ -0,0 +1,162 @@ | |||
| // Copyright 2014 The Flutter Authors. All rights reserved. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forget how this works: Does this benchmark need to be registered anywhere so it runs regularly and uploads data to skia perf? Or do we automatically run all benchmarks in dev/benchmarks/microbenchmarks/lib/ui?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added to microbenchmarks.dart
| import 'debug.dart'; | ||
| import 'layer.dart'; | ||
| import 'proxy_box.dart'; | ||
| import 'view.dart' show RenderView; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still a layer violation. RenderView sits on top of object.dart.
(not sure why we have the proxy_box.dart import here, it should be removed as well)
|
|
||
| /// {@template flutter.rendering.RenderObject.getTransformTo} | ||
| /// Applies the paint transform up the tree to `ancestor`. | ||
| /// Applies the paint transform from thie [RenderObject] to the `target` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: thie
| /// If `ancestor` is null, this method returns a matrix that maps from the | ||
| /// The `skipIfInUnpaintedSubtree` argument determines whether the method | ||
| /// should return a zero matrix when either this [RenderObject] or `target` is | ||
| /// in a subtree where the topmost [RenderObject]'s [RenderObject.paintsChild] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the topmost RenderObject?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code seems to check paintsChild for every RO along the path, so I am unsure what topmost means.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rephrased. Also changed the implementation
| /// in a subtree where the topmost [RenderObject]'s [RenderObject.paintsChild] | ||
| /// method returns false. In addition to that, this method may also return a | ||
| /// zero matrix when the paint transform from the common ancestor [RenderObject] | ||
| /// (of [this] and `target`) to `target` is a singular matrix. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does it mean in practical terms if it is a singular matrix? What structure would your render tree have if that happens?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rephrased. If any RenderObject in the common ancestor -> target RenderObject path uses a coordinate system orthogonal to that of its parent then we can't compute the paint transform using applyPaintTransform. I think it's a limitation of the applyPaintTransform API
| /// in a subtree where the topmost [RenderObject]'s [RenderObject.paintsChild] | ||
| /// method returns false. In addition to that, this method may also return a | ||
| /// zero matrix when the paint transform from the common ancestor [RenderObject] | ||
| /// (of [this] and `target`) to `target` is a singular matrix. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does dartdoc link to if you do [this]? The class doc for RenderObject?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah likely but I didn't mean to make that a link. Changed to this.
| for (int index = renderers.length - 1; index > 0; index -= 1) { | ||
| renderers[index].applyPaintTransform(renderers[index - 1], transform); | ||
|
|
||
| assert(toPath.length > 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If target == null couldn't the length of the toPath just be 1? what garantees that the while loop above executes at least twice to guarantee > 1?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
each list must have more than one element if not null.
| toPath[index].applyPaintTransform(toPath[index - 1], toTransform); | ||
| } | ||
| return transform; | ||
| if (toTransform.invert() == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I am a bit rusty on my matrix math, but why is it for the operation below important that it is invertible? Maybe leave a comment about that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation does nothing (https://master-api.flutter.dev/flutter/vector_math_64/Matrix4/copyInverse.html) when the matrix is singular. However I think that's because of the limitation of applyPaintTransform.
| mixin Selectable implements SelectionHandler { | ||
| /// {@macro flutter.rendering.RenderObject.getTransformTo} | ||
| Matrix4 getTransformTo(RenderObject? ancestor); | ||
| Matrix4 getTransformTo(RenderObject? target, { bool skipIfInUnpaintedSubtree = false }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For my own understanding: Why do we need to modify the signature on Selectable as well? Looks like nothing in this PR is actually calling this with the new parameter?
(also, this seems to be uncovered by tests if we need to keep it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Selectable.getTransformTo isn't a valid override of RenderObject.getTransformTo if a RenderObject uses that mixin. Alternatively I could change Selectable to an abstract class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's kinda unfortunate that Selectable and RenderObject interact that way when used together. It's not immediately clear to me why the Selectable interface (when looked at it stand-alone) would need this parameter. Should we have named this method something else (and make implementors forward it to the RenderObject's getTransformTo, if that's what they want to use)?
Or does the caller of this method on Selectable actually need to care about unpainted subtrees and should use this parameter?
@chunhtai Do you have any thoughts on this?
Alternatively I could change Selectable to an abstract class?
How would you then make a RenderObject subclass use this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no implementation in the Selectable mixin so I can turn that into an abstract class? It could be breaking since people would have to replace with with implements
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but wouldn't class Foo extends RenderObject implements Selectable still have the problem that you cannot satisfy both overrides?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is abstract class and this method have a different signature, they would have to manually wire it up when they create a renderobject that implements this abstract class.
what is the use case for skipIfInUnpaintedSubtree: false?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't class Foo extends RenderObject implements Selectable still have the problem that you cannot satisfy both overrides?
According to dartpad that works. My guess is with Selectable creates a synthetic class that tries to override getTransformTo(RenderObject, { bool ....}) with getTransformTo(RenderObject).
what is the use case for skipIfInUnpaintedSubtree: false
idk, maybe in an animation callback where a RenderOpacity's paintsChild returns false but will return true once the render object updates its layout in that frame?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would probably remove skipIfInUnpaintedSubtree and don't worry about the signature mismatch. It doesn't seem worth it to modify RenderObject API without a solid use case.
I am ok if we convert to abstract class, this seems like an oversight when i implement this class. but it doesn't seem to help much?
0edb40a to
18bede0
Compare
18bede0 to
0ea8353
Compare
goderbauer
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This basically looks good to me, except for the Selectable bit.
| mixin Selectable implements SelectionHandler { | ||
| /// {@macro flutter.rendering.RenderObject.getTransformTo} | ||
| Matrix4 getTransformTo(RenderObject? ancestor); | ||
| Matrix4 getTransformTo(RenderObject? target, { bool skipIfInUnpaintedSubtree = false }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's kinda unfortunate that Selectable and RenderObject interact that way when used together. It's not immediately clear to me why the Selectable interface (when looked at it stand-alone) would need this parameter. Should we have named this method something else (and make implementors forward it to the RenderObject's getTransformTo, if that's what they want to use)?
Or does the caller of this method on Selectable actually need to care about unpainted subtrees and should use this parameter?
@chunhtai Do you have any thoughts on this?
Alternatively I could change Selectable to an abstract class?
How would you then make a RenderObject subclass use this?
chunhtai
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case my latest comment is buried in the review, this is the link to my last comment #130192 (comment)
|
This pull request executed golden file tests, but it has not been updated in a while (20+ days). Test results from Gold expire after as many days, so this pull request will need to be updated with a fresh commit in order to get results from Gold. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
… in the same tree (flutter#148897) This is flutter#130192 but without the additional parameter. Fixes flutter#146764, flutter#148410
Basically a
getTransformTothat doesn't requireancestorto be an ancestor ofthis. This is prompted by #129969: whether there's an OverlayPortal between a descendant widget and an ancestor widget is sometimes an implementation detail, so the descendant widget or the ancestor widget should not make the assumption that their render objects are also a descendant/ancestor of each other.@chunhtai I'd like to use the new method to implement
Selectable.getTransformTonext. When the new method returnsnullthenSelectable.getTransformToshould return a zero matrix?Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.