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

RenderBox.{compute,get}DryBaseline #138369

Closed
wants to merge 31 commits into from

Conversation

LongCatIsLooong
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong commented Nov 13, 2023

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

  • 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 a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. f: cupertino flutter/packages/flutter/cupertino repository labels Nov 13, 2023
@LongCatIsLooong LongCatIsLooong force-pushed the text-intrinsics branch 2 times, most recently from ae99e7f to db4f937 Compare November 13, 2023 21:36
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.

I only looked at box.dart so far.

Comment on lines 1885 to 1887
/// 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
Copy link
Member

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

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

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?

Copy link
Member

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

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

Copy link
Contributor Author

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

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

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?

Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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?

Comment on lines -2360 to -2364
// 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.
Copy link
Member

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

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?

packages/flutter/lib/src/rendering/box.dart Show resolved Hide resolved
/// 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
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 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?

@goderbauer
Copy link
Member

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?

@LongCatIsLooong
Copy link
Contributor Author

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.

@goderbauer
Copy link
Member

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

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.

Review of the rest.

child = childAfter(child);
}
return baselineOffset;
}
Copy link
Member

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

Copy link
Contributor Author

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

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?

Copy link
Contributor Author

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.

Comment on lines 2254 to 2255
final double yAdjustment = clampDouble(childSize.height + padding.bottom - maxExtent, 0, padding.bottom);
final double offsetY = constraints.biggest.height - childSize.height - padding.bottom + yAdjustment;
Copy link
Member

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

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.

Copy link
Member

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

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

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.

Copy link
Member

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

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'm considering getting rid of this helper and banking on that the assert in debugPaint would catch any inconsistencies here.

Comment on lines 58 to 59
box.getDryBaseline(box.constraints, baseline),
box.debugGetDistanceToBaseline(baseline),
Copy link
Member

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?

Copy link
Member

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

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

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?

Copy link
Contributor Author

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

@goderbauer
Copy link
Member

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

@LongCatIsLooong LongCatIsLooong marked this pull request as draft November 29, 2023 09:15
@github-actions github-actions bot removed a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. f: cupertino flutter/packages/flutter/cupertino repository labels Dec 5, 2023
@github-actions github-actions bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. f: cupertino flutter/packages/flutter/cupertino repository labels Dec 6, 2023
@LongCatIsLooong
Copy link
Contributor Author

I think I've addressed most comments. Right now in debug mode, the RenderBox.getDryBaseline method still returns a valid baseline offset even if there's a LayoutBuilder descendent, if the RenderBox has been previously laid out at that input BoxConstraints and the baseline offset cache hasn't been invalidated. To fix that we'll need to distinguish "dry" value from "wet" value in the cache (but only in debug mode) so I'm not sure if it's worth implementing.

@LongCatIsLooong LongCatIsLooong marked this pull request as ready for review December 15, 2023 23:14
@LongCatIsLooong
Copy link
Contributor Author

Splitting this into smaller PRs.

auto-submit bot pushed a commit that referenced this pull request Mar 18, 2024
…without affecting the current layout (#144655)

Extracted from #138369

Introduces `RenderBox.{compute,get}DryBaseline` for computing the baseline location in `RenderBox.computeDryLayout`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: text input Entering text in a text field or keyboard related problems f: cupertino flutter/packages/flutter/cupertino repository 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.

None yet

2 participants