-
Notifications
You must be signed in to change notification settings - Fork 26.7k
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
RenderBox.{compute,get}DryBaseline
#138369
Conversation
ae99e7f
to
db4f937
Compare
db4f937
to
dafa737
Compare
7f53ebe
to
c6be509
Compare
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 only looked at box.dart
so far.
/// When the input constraints equal to the constraints given to this | ||
/// [RenderBox] by the parent, the returned baseline offset value is guaranteed | ||
/// to be consistent with the value returned by [getDistanceToBaseline]. It's |
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: I find it hard to follow what constraints you mean here by "input" constraints and "given" constraints. I assume it should say if the constraints passed to this method match the constraints that will be (or were) given to the render object during layout, then its return value is guaranteed to be consistent with getDistanceToBaseline. Or something like that.
return _computeDryLayout(constraints); | ||
Size getDryLayout(covariant BoxConstraints constraints) => _intrinsicsCache.getOrCompute(_IntrinsicsCacheType.dryLayout, constraints, _computeDryLayout); | ||
|
||
/// Returns the distance from the top of the box to the first baseline of the |
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.
Questions I have after reading this doc that I wish the doc would answer:
- What's the difference to getDistanceToBaseline? When should I call this new method, when the other one? (if the only difference is that one is destructive and the other one isn't, why do we still offer the destructive one?)
- getDistanceToBaseline has restrictions on when it can be called (only after layout), same for this one? Different restrictions?
- I am implementing my own RenderObject, what do I need to do to implement getDryBaseline (e.g. should I override this method or computeDryBaseline or something different)?
- What should I do if I cannot implement getDryBaseline for my custom render object (presumably, for certain layout builder situation, this cannot actually be implemented in a non-destructive way?) Also, what are common cases where this cannot be implemented?
- Maybe explain what makes this "dry" - what would be a "wet" baseline? See the corresponding explanation on "computeDryLayout".
@@ -1873,6 +1906,21 @@ abstract class RenderBox extends RenderObject { | |||
return result; | |||
} | |||
|
|||
double? _computeDryBaseline((BoxConstraints, TextBaseline) pair) { |
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: this would actually be slightly easier to read if the pair is passed in as individual arguments. Why bother with the pair?
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 see why you want the pair to make the computer signature in the cache work. This is fine then.
/// doing a full layout), it may be computed by a callback about which the | ||
/// render object cannot reason, or the layout is so complex that it | ||
/// is impractical to calculate the size in an efficient way. | ||
/// compute their size without affecting the current layout. For example, |
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.
"affecting the current layout" sounds a little misleading like it would actually change what's shown on screen. Wouldn't it in most cases just invalidate the layout and cause a bunch of additional work to recompute it at layout time? Maybe we could say "without invalidating the current layout" (maybe with the addition of saying something about how this would case extra work during layout).
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.
Reverted the change and deleted the part about baseline.
/// a [LayoutBuilder] changes its live render subtree based on the incoming | ||
/// constraints, so currently there is no easy way to compute its size | ||
/// speculatively given arbitrary constraints, for that alters the [RenderBox]'s | ||
/// current layout. |
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: I find the old explanation (minus the baseline talk) a little more approachable. It's not just THE [LayoutBuilder] widget thats affected by this. Any widget that invokes a layout callback (about which the render object cannot reason) has this problem and the note about complexity IMO still holds as well.
/// non-debug modes. | ||
@nonVirtual | ||
@visibleForTesting | ||
double? debugGetDistanceToBaseline(TextBaseline baseline) { |
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.
It's not clear to me why this method exists. Why not just call getDistanceToBaseline
directly instead of 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.
Also, do we actually want a debug method to insert things in the cache and with that change future behavior? Seems maybe a little dangerous?
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.
getDistanceToBaseline
can't be called in tests (there are a few asserts that would fire if you call that method during the idle phase).
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.
Add that information to the doc comment?
// If we have cached data, then someone must have used our data. | ||
// Since the parent will shortly be marked dirty, we can forget that they | ||
// used the baseline and/or intrinsic dimensions. If they use them again, | ||
// then we'll fill the cache again, and if we get dirty again, we'll | ||
// notify them again. |
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 you keep a comment along these lines on the new clear method of the cache that documents why it is ok to just forget the cached data?
if (_clearCachedData() && parent is RenderObject) { | ||
// A render object's performLayout implementation may depend on the baseline | ||
// location or the intrinsic dimensions of a descendant, even when there are | ||
// relayout boundaries between them. The cache being non-null implies 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 cache being non-null" is not super intuitive to understand here since the following lines don't check any caches for null. Maybe be a bit more explicit here why a true
return value of clear
means we have to mark the parent dirty?
/// This method calls [computeDryBaseline] under the hood and caches the result. | ||
/// When the input constraints equal to the constraints given to this | ||
/// [RenderBox] by the parent, the returned baseline offset value is guaranteed | ||
/// to be consistent with the value returned by [getDistanceToBaseline]. It's |
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 auto-test that actual implementations actually fulfill this guarantee? We have the debugCheckIntrinsicSizes
(enabled in our tests) that enables a bunch of extra asserts to check that the intrinsics calculations match expectations. Could we add an extra check that compares getDryBaseline
and getDistanceToBaseline
?
Can you remind me (and also add it to the PR description) what problem we can now solve by having the ability to compute baselines without non-destructively? Wasn't there some kind of an issue on file that we could link this PR to as a step towards resolving that issue? |
#71687 would be an example. But the PR by itself doesn't fix anything. The intrinsics methods have to be updated to use the new method(s) and I'm planning to do that in a different PR. |
Could you still mention that issue in the PR description and indicate that this is work towards fixing the issue to leave some breadcrumbs/context for future archeologists? |
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.
Review of the rest.
child = childAfter(child); | ||
} | ||
return baselineOffset; | ||
} |
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 one looks very similar to the previous one, if this is a general pattern, maybe we should provide a helper e.g. in RenderBoxContainerDefaultsMixin?)
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.
It's quite specific, the 2 widgets are similar (newer / older versions of the same UI component).
return null; | ||
} | ||
final Size childSize = child.getDryLayout(enforcedConstraint); | ||
final bool isAbove = anchorAbove.dy >= childSize.height - _kToolbarArrowSize.height * 2; |
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.
As said elsewhere, my main worry here is the amount of duplication that this introduces. How are we going to remember to fix up this line if we change the "wet" getter "isAbove" below? Same holds true for the enforcedConstraints above. This looks like it could be a maintenance nightmare. Is there a way to share more logic between the wet and dry implementation, similarly to what we are trying to do with performLayout and computeDryLayout for individual RenderObjects?
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.
How are we going to remember to fix up this line if we change the "wet" getter "isAbove" below?
Added a check in RenderBox.debugPaint
(can't do that in layout because some children may still be dirty) that verifies the getDryBaseline
and getDistanceToBaseline
return the same value, and another check in RenderBox.size
getter to prevent access to size
or child.size
during dry baseline calculation.
the amount of duplication
Moved the child positioning code to a common method.
final double yAdjustment = clampDouble(childSize.height + padding.bottom - maxExtent, 0, padding.bottom); | ||
final double offsetY = constraints.biggest.height - childSize.height - padding.bottom + yAdjustment; |
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.
Again, these lines appear to just be copied over from performLayout, EXCEPT to my initial surprise size.height has been replaced with constraints.biggest.height. If you study the code carefully, it will become clear that size.height
== constraints.biggest.height
for this particular render object, but it adds mental overhead to the maintainability and introduces further room for bugs.
@@ -325,6 +325,15 @@ class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin<Ren | |||
return Size.zero; | |||
} | |||
|
|||
@override | |||
double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) { | |||
assert(debugCannotComputeDryLayout(reason: |
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.
Doc on debugCannotComputeDryLayout needs to be updated if this may also be called from debugDryBaseline.
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.
(interestingly, debugCannotComputeDryLayout seems to be mostly called from dryLayout implementations that need baseline metrics)
@override | ||
double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) { | ||
assert(debugCannotComputeDryLayout(reason: | ||
'Calculating the dry layout would require running the layout callback ' |
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.
should this be specific about dry baseline instead of just generically talking about dry layout? (same for the default ErrorSummary that debugCannotComputeDryLayout produces)
} | ||
} | ||
|
||
double? verifyDryBaseline(RenderBox box) { |
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 method could benefit from a doc 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.
Just from the method name I would have expected that this method would only contain the expect on line 57. It is not clear to me what the beforePaintInstructions/afterPaintInstructions and their associated expects are supposed to test? And the return value is also somewhat surprising (to me it implies that I need to compare it with something in my test in an expect, but the expects seem to be build into this methods...)
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'm considering getting rid of this helper and banking on that the assert in debugPaint
would catch any inconsistencies here.
box.getDryBaseline(box.constraints, baseline), | ||
box.debugGetDistanceToBaseline(baseline), |
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.
Isn't getDryBaseline just putting the computed baseline into the cache and debugGetDistanceToBaseline retrieving it from there without redoing the actual calculation? Wouldn't we want to test the implementations behind these instead of just testing the caching behavior?
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 it would be worthwhile to add a test for this util that makes sure RenderObjects with non-matching implementations fail this check?
); | ||
|
||
expect(beforePaintInstructions, equals(afterPaintInstructions.map(_PaintInstructionMatcher.new))); | ||
return box.getDryBaseline(box.constraints, TextBaseline.alphabetic); |
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.
Why the alphabetic instead of the other one?
}), | ||
paintsNothing, | ||
), | ||
); |
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.
Help me, what is this expect supposed to be asserting?
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.
Sometimes the render object doesn't paint anything so that has to be matched using paintsNothing
.
import 'package:flutter/rendering.dart'; | ||
import 'package:flutter_test/flutter_test.dart'; | ||
|
||
class _PaintInstructionMatcher extends Matcher { |
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 this matcher provide a clear error message when it fails? I see a lot of our custom matcher implement describeMismatch to help out with that, but this one doesn't.
How did you decide which RenderBoxes need a custom computeDryBaseline implementation? How should I as a custom render object author determine that I need one (the latter should be added to the docs somewhere)? |
7a9c27b
to
9d9bb92
Compare
I think I've addressed most comments. Right now in debug mode, the |
Splitting this into smaller PRs. |
Introduce methods for computing the baseline location of a
RenderBox
without affecting the current layout (see packages/flutter/lib/src/rendering/box.dart for the signatures).In follow-up PRs, the new methods will be called mostly in
computeDryLayout
for RenderBoxes with baseline-aligned children. Unfortunately LayoutBuilders still don't have the ability to compute the intrinsic dimensions sideeffect-free.Issues that depend on this:
#138794
#71687
Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.