Skip to content

Conversation

@LongCatIsLooong
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong commented Jul 8, 2023

Basically a getTransformTo that doesn't require ancestor to be an ancestor of this. 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.getTransformTo next. When the new method returns null then Selectable.getTransformTo should return a zero matrix?

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.

@github-actions github-actions bot added c: contributor-productivity Team-specific productivity, code health, technical debt. framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. labels Jul 8, 2023
return transform;
}

/// Returns the paint transform that allows this `RenderObject` to perform
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// Returns the paint transform that allows this `RenderObject` to perform
/// Returns the paint transform that allows this [RenderObject] to perform

Copy link
Member

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) {
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: Why does this need new API instead of just making getTransformTo behave this way?

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 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) {
Copy link
Member

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.

Copy link
Member

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.

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

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

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) {
Copy link
Member

Choose a reason for hiding this comment

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

typo - I think?

Suggested change
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) {
Copy link
Member

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.

Comment on lines 3261 to 3264
return _computePaintTransform(ancestor, identity);
} else {
final RenderObject? parent = this.parent;
return parent != null ? _computePaintTransformToGlobalCoordinates(parent, identity) : identity;
Copy link
Member

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...

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 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)?

Copy link
Member

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) {
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: 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()));
});
Copy link
Member

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);
Copy link
Member

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()? 😄

@LongCatIsLooong
Copy link
Contributor Author

Updated the code and stuck to the getTransformTo name. I had to change the Selectable interface such that its getTransformTo also takes a named parameter.

@@ -0,0 +1,162 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
Copy link
Member

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?

Copy link
Contributor Author

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;
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 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`
Copy link
Member

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

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?

Copy link
Member

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.

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

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?

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

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?

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 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);
Copy link
Member

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?

Copy link
Contributor Author

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) {
Copy link
Member

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?

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 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 });
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: 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)

Copy link
Contributor Author

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?

Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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?

Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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?

@LongCatIsLooong LongCatIsLooong force-pushed the RenderObject.getTransformTo branch from 0edb40a to 18bede0 Compare July 16, 2023 12:19
@LongCatIsLooong LongCatIsLooong force-pushed the RenderObject.getTransformTo branch from 18bede0 to 0ea8353 Compare July 16, 2023 20:06
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.

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 });
Copy link
Member

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?

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.

In case my latest comment is buried in the review, this is the link to my last comment #130192 (comment)

@flutter-dashboard
Copy link

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 package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

auto-submit bot pushed a commit that referenced this pull request May 22, 2024
… in the same tree (#148897)

This is #130192 but without the additional parameter.

Fixes #146764, #148410
victorsanni pushed a commit to victorsanni/flutter that referenced this pull request May 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: contributor-productivity Team-specific productivity, code health, technical debt. 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.

3 participants