Introduce methods for computing the baseline location of a RenderBox without affecting the current layout (#144655)

Extracted from https://github.com/flutter/flutter/pull/138369

Introduces `RenderBox.{compute,get}DryBaseline` for computing the baseline location in `RenderBox.computeDryLayout`.
This commit is contained in:
LongCatIsLooong 2024-03-18 14:32:22 -07:00 committed by GitHub
parent f704560c0b
commit 98369bdd50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 653 additions and 227 deletions

View file

@ -1316,7 +1316,10 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
@override @override
double computeDistanceToActualBaseline(TextBaseline baseline) { double computeDistanceToActualBaseline(TextBaseline baseline) {
return _boxParentData(input!).offset.dy + (input?.computeDistanceToActualBaseline(baseline) ?? 0.0); final RenderBox? input = this.input;
return input == null
? 0.0
: _boxParentData(input).offset.dy + (input.computeDistanceToActualBaseline(baseline) ?? 0.0);
} }
// Records where the label was painted. // Records where the label was painted.

View file

@ -1249,10 +1249,12 @@ class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_
} }
@override @override
double computeDistanceToActualBaseline(TextBaseline baseline) { double? computeDistanceToActualBaseline(TextBaseline baseline) {
assert(title != null); assert(title != null);
final BoxParentData parentData = title!.parentData! as BoxParentData; final BoxParentData parentData = title!.parentData! as BoxParentData;
return parentData.offset.dy + title!.getDistanceToActualBaseline(baseline)!; final BaselineOffset offset = BaselineOffset(title!.getDistanceToActualBaseline(baseline))
+ parentData.offset.dy;
return offset.offset;
} }
static double? _boxBaseline(RenderBox box, TextBaseline baseline) { static double? _boxBaseline(RenderBox box, TextBaseline baseline) {

View file

@ -1175,11 +1175,13 @@ class _SelectToggleButtonRenderObject extends RenderShiftedBox {
} }
@override @override
double computeDistanceToActualBaseline(TextBaseline baseline) { double? computeDistanceToActualBaseline(TextBaseline baseline) {
// The baseline of this widget is the baseline of its child // The baseline of this widget is the baseline of its child
return direction == Axis.horizontal final BaselineOffset childOffset = BaselineOffset(child?.computeDistanceToActualBaseline(baseline));
? child!.computeDistanceToActualBaseline(baseline)! + borderSide.width return switch (direction) {
: child!.computeDistanceToActualBaseline(baseline)! + leadingBorderSide.width; Axis.horizontal => childOffset + borderSide.width,
Axis.vertical => childOffset + leadingBorderSide.width,
}.offset;
} }
@override @override

View file

@ -192,7 +192,7 @@ class BoxConstraints extends Constraints {
} }
/// Returns new box constraints that are smaller by the given edge dimensions. /// Returns new box constraints that are smaller by the given edge dimensions.
BoxConstraints deflate(EdgeInsets edges) { BoxConstraints deflate(EdgeInsetsGeometry edges) {
assert(debugAssertIsValid()); assert(debugAssertIsValid());
final double horizontal = edges.horizontal; final double horizontal = edges.horizontal;
final double vertical = edges.vertical; final double vertical = edges.vertical;
@ -940,24 +940,154 @@ class BoxParentData extends ParentData {
/// the relevant type arguments. /// the relevant type arguments.
abstract class ContainerBoxParentData<ChildType extends RenderObject> extends BoxParentData with ContainerParentDataMixin<ChildType> { } abstract class ContainerBoxParentData<ChildType extends RenderObject> extends BoxParentData with ContainerParentDataMixin<ChildType> { }
enum _IntrinsicDimension { minWidth, maxWidth, minHeight, maxHeight } /// A wrapper that represents the baseline location of a `RenderBox`.
extension type const BaselineOffset(double? offset) {
/// A value that indicates that the associated `RenderBox` does not have any
/// baselines.
///
/// [BaselineOffset.noBaseline] is an identity element in most binary
/// operations involving two [BaselineOffset]s (such as [minOf]), for render
/// objects with no baselines typically do not contribute to the baseline
/// offset of their parents.
static const BaselineOffset noBaseline = BaselineOffset(null);
@immutable /// Returns a new baseline location that is `offset` pixels further away from
class _IntrinsicDimensionsCacheEntry { /// the origin than `this`, or unchanged if `this` is [noBaseline].
const _IntrinsicDimensionsCacheEntry(this.dimension, this.argument); BaselineOffset operator +(double offset) {
final double? value = this.offset;
return BaselineOffset(value == null ? null : value + offset);
}
final _IntrinsicDimension dimension; /// Compares this [BaselineOffset] and `other`, and returns whichever is closer
final double argument; /// to the origin.
///
/// When both `this` and `other` are [noBaseline], this method returns
/// [noBaseline]. When one of them is [noBaseline], this method returns the
/// other oprand that's not [noBaseline].
BaselineOffset minOf(BaselineOffset other) {
return switch ((this, other)) {
(final double lhs?, final double rhs?) => lhs >= rhs ? other : this,
(final double lhs?, null) => BaselineOffset(lhs),
(null, final BaselineOffset rhs) => rhs,
};
}
}
/// An interface that represents a memoized layout computation run by a [RenderBox].
///
/// Each subclass is inhabited by a single object. Each object represents the
/// signature of a memoized layout computation run by [RenderBox]. For instance,
/// the [dryLayout] object of the [_DryLayout] subclass represents the signature
/// of the [RenderBox.computeDryLayout] method: it takes a [BoxConstraints] (the
/// subclass's `Input` type parameter) and returns a [Size] (the subclass's
/// `Output` type parameter).
///
/// Subclasses do not own their own cache storage. Rather, their [memoize]
/// implementation takes a `cacheStorage`. If a prior computation with the same
/// input valus has already been memoized in `cacheStorage`, it returns the
/// memoized value without running `computer`. Otherwise the method runs the
/// `computer` to compute the return value, and caches the result to
/// `cacheStorage`.
///
/// The layout cache storage is typically cleared in `markNeedsLayout`, but is
/// usually kept across [RenderObject.layout] calls because the incoming
/// [BoxConstraints] is always an input of every layout computation.
abstract class _CachedLayoutCalculation<Input extends Object, Output> {
static const _DryLayout dryLayout = _DryLayout();
static const _Baseline baseline = _Baseline();
Output memoize(_LayoutCacheStorage cacheStorage, Input input, Output Function(Input) computer);
// Debug information that will be used to generate the Timeline event for this type of calculation.
Map<String, String> debugFillTimelineArguments(Map<String, String> timelineArguments, Input input);
String eventLabel(RenderBox renderBox);
}
final class _DryLayout implements _CachedLayoutCalculation<BoxConstraints, Size> {
const _DryLayout();
@override @override
bool operator ==(Object other) { Size memoize(_LayoutCacheStorage cacheStorage, BoxConstraints input, Size Function(BoxConstraints) computer) {
return other is _IntrinsicDimensionsCacheEntry return (cacheStorage._cachedDryLayoutSizes ??= <BoxConstraints, Size>{}).putIfAbsent(input, () => computer(input));
&& other.dimension == dimension
&& other.argument == argument;
} }
@override @override
int get hashCode => Object.hash(dimension, argument); Map<String, String> debugFillTimelineArguments(Map<String, String> timelineArguments, BoxConstraints input) {
return timelineArguments..['getDryLayout constraints'] = '$input';
}
@override
String eventLabel(RenderBox renderBox) => '${renderBox.runtimeType}.getDryLayout';
}
final class _Baseline implements _CachedLayoutCalculation<(BoxConstraints, TextBaseline), BaselineOffset> {
const _Baseline();
@override
BaselineOffset memoize(_LayoutCacheStorage cacheStorage, (BoxConstraints, TextBaseline) input, BaselineOffset Function((BoxConstraints, TextBaseline)) computer) {
final Map<BoxConstraints, BaselineOffset> cache = switch (input.$2) {
TextBaseline.alphabetic => cacheStorage._cachedAlphabeticBaseline ??= <BoxConstraints, BaselineOffset>{},
TextBaseline.ideographic => cacheStorage._cachedIdeoBaseline ??= <BoxConstraints, BaselineOffset>{},
};
BaselineOffset ifAbsent() => computer(input);
return cache.putIfAbsent(input.$1, ifAbsent);
}
@override
Map<String, String> debugFillTimelineArguments(Map<String, String> timelineArguments, (BoxConstraints, TextBaseline) input) {
return timelineArguments
..['baseline type'] = '${input.$2}'
..['constraints'] = '${input.$1}';
}
@override
String eventLabel(RenderBox renderBox) => '${renderBox.runtimeType}.getDryBaseline';
}
// Intrinsic dimension calculation that computes the intrinsic width given the
// max height, or the intrinsic height given the max width.
enum _IntrinsicDimension implements _CachedLayoutCalculation<double, double> {
minWidth, maxWidth, minHeight, maxHeight;
@override
double memoize(_LayoutCacheStorage cacheStorage, double input, double Function(double) computer) {
return (cacheStorage._cachedIntrinsicDimensions ??= <(_IntrinsicDimension, double), double>{})
.putIfAbsent((this, input), () => computer(input));
}
@override
Map<String, String> debugFillTimelineArguments(Map<String, String> timelineArguments, double input) {
return timelineArguments
..['intrinsics dimension'] = name
..['intrinsics argument'] = '$input';
}
@override
String eventLabel(RenderBox renderBox) => '${renderBox.runtimeType} intrinsics';
}
final class _LayoutCacheStorage {
Map<(_IntrinsicDimension, double), double>? _cachedIntrinsicDimensions;
Map<BoxConstraints, Size>? _cachedDryLayoutSizes;
Map<BoxConstraints, BaselineOffset>? _cachedAlphabeticBaseline;
Map<BoxConstraints, BaselineOffset>? _cachedIdeoBaseline;
// Returns a boolean indicating whether the cache storage has cached
// intrinsics / dry layout data in it.
bool clear() {
final bool hasCache = (_cachedDryLayoutSizes?.isNotEmpty ?? false)
|| (_cachedIntrinsicDimensions?.isNotEmpty ?? false)
|| (_cachedAlphabeticBaseline?.isNotEmpty ?? false)
|| (_cachedIdeoBaseline?.isNotEmpty ?? false);
if (hasCache) {
_cachedDryLayoutSizes?.clear();
_cachedIntrinsicDimensions?.clear();
_cachedAlphabeticBaseline?.clear();
_cachedIdeoBaseline?.clear();
}
return hasCache;
}
} }
/// A render object in a 2D Cartesian coordinate system. /// A render object in a 2D Cartesian coordinate system.
@ -1381,55 +1511,52 @@ abstract class RenderBox extends RenderObject {
} }
} }
Map<_IntrinsicDimensionsCacheEntry, double>? _cachedIntrinsicDimensions; final _LayoutCacheStorage _layoutCacheStorage = _LayoutCacheStorage();
static int _debugIntrinsicsDepth = 0;
double _computeIntrinsicDimension(_IntrinsicDimension dimension, double argument, double Function(double argument) computer) { static int _debugIntrinsicsDepth = 0;
Output _computeIntrinsics<Input extends Object, Output>(
_CachedLayoutCalculation<Input, Output> type,
Input input,
Output Function(Input) computer,
) {
assert(RenderObject.debugCheckingIntrinsics || !debugDoingThisResize); // performResize should not depend on anything except the incoming constraints assert(RenderObject.debugCheckingIntrinsics || !debugDoingThisResize); // performResize should not depend on anything except the incoming constraints
bool shouldCache = true; bool shouldCache = true;
assert(() { assert(() {
// we don't want the checked-mode intrinsic tests to affect // we don't want the checked-mode intrinsic tests to affect
// who gets marked dirty, etc. // who gets marked dirty, etc.
if (RenderObject.debugCheckingIntrinsics) { shouldCache = !RenderObject.debugCheckingIntrinsics;
shouldCache = false;
}
return true; return true;
}()); }());
if (shouldCache) { return shouldCache ? _computeWithTimeline(type, input, computer) : computer(input);
Map<String, String>? debugTimelineArguments; }
assert(() {
if (debugEnhanceLayoutTimelineArguments) { Output _computeWithTimeline<Input extends Object, Output>(
debugTimelineArguments = toDiagnosticsNode().toTimelineArguments(); _CachedLayoutCalculation<Input, Output> type,
} else { Input input,
debugTimelineArguments = <String, String>{}; Output Function(Input) computer,
} ) {
debugTimelineArguments!['intrinsics dimension'] = dimension.name; Map<String, String>? debugTimelineArguments;
debugTimelineArguments!['intrinsics argument'] = '$argument'; assert(() {
return true; final Map<String, String> arguments = debugEnhanceLayoutTimelineArguments
}()); ? toDiagnosticsNode().toTimelineArguments()!
if (!kReleaseMode) { : <String, String>{};
if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) { debugTimelineArguments = type.debugFillTimelineArguments(arguments, input);
FlutterTimeline.startSync( return true;
'$runtimeType intrinsics', }());
arguments: debugTimelineArguments, if (!kReleaseMode) {
); if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) {
} FlutterTimeline.startSync(type.eventLabel(this), arguments: debugTimelineArguments);
_debugIntrinsicsDepth += 1;
} }
_cachedIntrinsicDimensions ??= <_IntrinsicDimensionsCacheEntry, double>{}; _debugIntrinsicsDepth += 1;
final double result = _cachedIntrinsicDimensions!.putIfAbsent(
_IntrinsicDimensionsCacheEntry(dimension, argument),
() => computer(argument),
);
if (!kReleaseMode) {
_debugIntrinsicsDepth -= 1;
if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) {
FlutterTimeline.finishSync();
}
}
return result;
} }
return computer(argument); final Output result = type.memoize(_layoutCacheStorage, input, computer);
if (!kReleaseMode) {
_debugIntrinsicsDepth -= 1;
if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) {
FlutterTimeline.finishSync();
}
}
return result;
} }
/// Returns the minimum width that this box could be without failing to /// Returns the minimum width that this box could be without failing to
@ -1463,7 +1590,7 @@ abstract class RenderBox extends RenderObject {
} }
return true; return true;
}()); }());
return _computeIntrinsicDimension(_IntrinsicDimension.minWidth, height, computeMinIntrinsicWidth); return _computeIntrinsics(_IntrinsicDimension.minWidth, height, computeMinIntrinsicWidth);
} }
/// Computes the value returned by [getMinIntrinsicWidth]. Do not call this /// Computes the value returned by [getMinIntrinsicWidth]. Do not call this
@ -1605,7 +1732,7 @@ abstract class RenderBox extends RenderObject {
} }
return true; return true;
}()); }());
return _computeIntrinsicDimension(_IntrinsicDimension.maxWidth, height, computeMaxIntrinsicWidth); return _computeIntrinsics(_IntrinsicDimension.maxWidth, height, computeMaxIntrinsicWidth);
} }
/// Computes the value returned by [getMaxIntrinsicWidth]. Do not call this /// Computes the value returned by [getMaxIntrinsicWidth]. Do not call this
@ -1681,7 +1808,7 @@ abstract class RenderBox extends RenderObject {
} }
return true; return true;
}()); }());
return _computeIntrinsicDimension(_IntrinsicDimension.minHeight, width, computeMinIntrinsicHeight); return _computeIntrinsics(_IntrinsicDimension.minHeight, width, computeMinIntrinsicHeight);
} }
/// Computes the value returned by [getMinIntrinsicHeight]. Do not call this /// Computes the value returned by [getMinIntrinsicHeight]. Do not call this
@ -1756,7 +1883,7 @@ abstract class RenderBox extends RenderObject {
} }
return true; return true;
}()); }());
return _computeIntrinsicDimension(_IntrinsicDimension.maxHeight, width, computeMaxIntrinsicHeight); return _computeIntrinsics(_IntrinsicDimension.maxHeight, width, computeMaxIntrinsicHeight);
} }
/// Computes the value returned by [getMaxIntrinsicHeight]. Do not call this /// Computes the value returned by [getMaxIntrinsicHeight]. Do not call this
@ -1800,9 +1927,6 @@ abstract class RenderBox extends RenderObject {
return 0.0; return 0.0;
} }
Map<BoxConstraints, Size>? _cachedDryLayoutSizes;
bool _computingThisDryLayout = false;
/// Returns the [Size] that this [RenderBox] would like to be given the /// Returns the [Size] that this [RenderBox] would like to be given the
/// provided [BoxConstraints]. /// provided [BoxConstraints].
/// ///
@ -1822,49 +1946,11 @@ abstract class RenderBox extends RenderObject {
/// ///
/// Do not override this method. Instead, implement [computeDryLayout]. /// Do not override this method. Instead, implement [computeDryLayout].
@mustCallSuper @mustCallSuper
Size getDryLayout(BoxConstraints constraints) { Size getDryLayout(covariant BoxConstraints constraints) {
bool shouldCache = true; return _computeIntrinsics(_CachedLayoutCalculation.dryLayout, constraints, _computeDryLayout);
assert(() {
// we don't want the checked-mode intrinsic tests to affect
// who gets marked dirty, etc.
if (RenderObject.debugCheckingIntrinsics) {
shouldCache = false;
}
return true;
}());
if (shouldCache) {
Map<String, String>? debugTimelineArguments;
assert(() {
if (debugEnhanceLayoutTimelineArguments) {
debugTimelineArguments = toDiagnosticsNode().toTimelineArguments();
} else {
debugTimelineArguments = <String, String>{};
}
debugTimelineArguments!['getDryLayout constraints'] = '$constraints';
return true;
}());
if (!kReleaseMode) {
if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) {
FlutterTimeline.startSync(
'$runtimeType.getDryLayout',
arguments: debugTimelineArguments,
);
}
_debugIntrinsicsDepth += 1;
}
_cachedDryLayoutSizes ??= <BoxConstraints, Size>{};
final Size result = _cachedDryLayoutSizes!.putIfAbsent(constraints, () => _computeDryLayout(constraints));
if (!kReleaseMode) {
_debugIntrinsicsDepth -= 1;
if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) {
FlutterTimeline.finishSync();
}
}
return result;
}
return _computeDryLayout(constraints);
} }
bool _computingThisDryLayout = false;
Size _computeDryLayout(BoxConstraints constraints) { Size _computeDryLayout(BoxConstraints constraints) {
assert(() { assert(() {
assert(!_computingThisDryLayout); assert(!_computingThisDryLayout);
@ -1902,11 +1988,8 @@ abstract class RenderBox extends RenderObject {
/// ### When the size cannot be known /// ### When the size cannot be known
/// ///
/// There are cases where render objects do not have an efficient way to /// There are cases where render objects do not have an efficient way to
/// compute their size without doing a full layout. For example, the size /// compute their size. For example, the size may computed by a callback about
/// may depend on the baseline of a child (which is not available without /// which the render object cannot reason.
/// 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.
/// ///
/// In such cases, it may be impossible (or at least impractical) to actually /// In such cases, it may be impossible (or at least impractical) to actually
/// return a valid answer. In such cases, the function should call /// return a valid answer. In such cases, the function should call
@ -1926,10 +2009,108 @@ abstract class RenderBox extends RenderObject {
return Size.zero; return Size.zero;
} }
static bool _dryLayoutCalculationValid = true; /// Returns the distance from the top of the box to the first baseline of the
/// box's contents for the given `constraints`, or `null` if this [RenderBox]
/// does not have any baselines.
///
/// This method calls [computeDryBaseline] under the hood and caches the result.
/// [RenderBox] subclasses typically don't overridden [getDryBaseline]. Instead,
/// consider overriding [computeDryBaseline] such that it returns a baseline
/// location that is consistent with [getDistanceToActualBaseline]. See the
/// documentation for the [computeDryBaseline] method for more details.
///
/// This method is usually called by the [computeDryBaseline] or the
/// [computeDryLayout] implementation of a parent [RenderBox] to get the
/// baseline location of a [RenderBox] child. Unlike [getDistanceToBaseline],
/// this method takes a [BoxConstraints] as an argument and computes the
/// baseline location as if the [RenderBox] was laid out by the parent using
/// that [BoxConstraints].
///
/// The "dry" in the method name means this method, like [getDryLayout], has
/// no observable side effects when called, as opposed to "wet" layout methods
/// such as [performLayout] (which changes this [RenderBox]'s [size], and the
/// offsets of its children if any). Since this method does not depend on the
/// current layout, unlike [getDistanceToBaseline], it's ok to call this method
/// when this [RenderBox]'s layout is outdated.
///
/// Similar to the intrinsic width/height and [getDryLayout], calling this
/// function in [performLayout] is expensive, as it can result in O(N^2) layout
/// performance, where N is the number of render objects in the render subtree.
/// Typically this method should be only called by the parent [RenderBox]'s
/// [computeDryBaseline] or [computeDryLayout] implementation.
double? getDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
final double? baselineOffset = _computeIntrinsics(_CachedLayoutCalculation.baseline, (constraints, baseline), _computeDryBaseline).offset;
// This assert makes sure computeDryBaseline always gets called in debug mode,
// in case the computeDryBaseline implementation invokes debugCannotComputeDryLayout.
assert(baselineOffset == computeDryBaseline(constraints, baseline));
return baselineOffset;
}
/// Called from [computeDryLayout] within an assert if the given [RenderBox] bool _computingThisDryBaseline = false;
/// subclass does not support calculating a dry layout. BaselineOffset _computeDryBaseline((BoxConstraints, TextBaseline) pair) {
assert(() {
assert(!_computingThisDryBaseline);
_computingThisDryBaseline = true;
return true;
}());
final BaselineOffset result = BaselineOffset(computeDryBaseline(pair.$1, pair.$2));
assert(() {
assert(_computingThisDryBaseline);
_computingThisDryBaseline = false;
return true;
}());
return result;
}
/// Computes the value returned by [getDryBaseline].
///
/// This method is for overriding only and shouldn't be called directly. To
/// get this [RenderBox]'s speculative baseline location for the given
/// `constraints`, call [getDryBaseline] instead.
///
/// The "dry" in the method name means the implementation must not produce
/// observable side effects when called. For example, it must not change the
/// [size] of the [RenderBox], or its children's paint offsets, otherwise that
/// would results in UI changes when [paint] is called, or hit-testing behavior
/// changes when [hitTest] is called. Moreover, accessing the current layout
/// of this [RenderBox] or child [RenderBox]es (including accessing [size], or
/// `child.size`) usually indicates a bug in the implementaion, as the current
/// layout is typically calculated using a set of [BoxConstraints] that's
/// different from the `constraints` given as the first parameter. To get the
/// size of this [RenderBox] or a child [RenderBox] in this method's
/// implementatin, use the [getDryLayout] method instead.
///
/// The implementation must return a value that represents the distance from
/// the top of the box to the first baseline of the box's contents, for the
/// given `constraints`, or `null` if the [RenderBox] has no baselines. It's
/// the same exact value [RenderBox.computeDistanceToActualBaseline] would
/// return, when this [RenderBox] was laid out at `constraints` in the same
/// exact state.
///
/// Not all [RenderBox]es support dry baseline computation. For example, to
/// compute the dry baseline of a [LayoutBuilder], its `builder` may have to
/// be called with different constraints, which may have side effects such as
/// updating the widget tree, violating the "dry" contract. In such cases the
/// [RenderBox] must call [debugCannotComputeDryLayout] in an assert, and
/// return a dummy baseline offset value (such as `null`).
@protected
double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
assert(debugCannotComputeDryLayout(
error: FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The ${objectRuntimeType(this, 'RenderBox')} class does not implement "computeDryBaseline".'),
ErrorHint(
'If you are not writing your own RenderBox subclass, then this is not\n'
'your fault. Contact support: https://github.com/flutter/flutter/issues/new?template=2_bug.yml',
),
]),
));
return null;
}
static bool _debugDryLayoutCalculationValid = true;
/// Called from [computeDryLayout] or [computeDryBaseline] within an assert if
/// the given [RenderBox] subclass does not support calculating a dry layout.
/// ///
/// When asserts are enabled and [debugCheckingIntrinsics] is not true, this /// When asserts are enabled and [debugCheckingIntrinsics] is not true, this
/// method will either throw the provided [FlutterError] or it will create and /// method will either throw the provided [FlutterError] or it will create and
@ -1947,7 +2128,7 @@ abstract class RenderBox extends RenderObject {
assert(() { assert(() {
if (!RenderObject.debugCheckingIntrinsics) { if (!RenderObject.debugCheckingIntrinsics) {
if (reason != null) { if (reason != null) {
assert(error ==null); assert(error == null);
throw FlutterError.fromParts(<DiagnosticsNode>[ throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The ${objectRuntimeType(this, 'RenderBox')} class does not support dry layout.'), ErrorSummary('The ${objectRuntimeType(this, 'RenderBox')} class does not support dry layout.'),
if (reason.isNotEmpty) ErrorDescription(reason), if (reason.isNotEmpty) ErrorDescription(reason),
@ -1956,7 +2137,7 @@ abstract class RenderBox extends RenderObject {
assert(error != null); assert(error != null);
throw error!; throw error!;
} }
_dryLayoutCalculationValid = false; _debugDryLayoutCalculationValid = false;
return true; return true;
}()); }());
return true; return true;
@ -1981,19 +2162,33 @@ abstract class RenderBox extends RenderObject {
final Size? size = _size; final Size? size = _size;
if (size is _DebugSize) { if (size is _DebugSize) {
assert(size._owner == this); assert(size._owner == this);
if (RenderObject.debugActiveLayout != null && final RenderObject? parent = this.parent;
!RenderObject.debugActiveLayout!.debugDoingThisLayoutWithCallback) { // Whether the size getter is accessed during layout (but not in a
assert( // layout callback).
debugDoingThisResize || debugDoingThisLayout || _computingThisDryLayout || final bool doingRegularLayout = !(RenderObject.debugActiveLayout?.debugDoingThisLayoutWithCallback ?? true);
(RenderObject.debugActiveLayout == parent && size._canBeUsedByParent), final bool sizeAccessAllowed = !doingRegularLayout
'RenderBox.size accessed beyond the scope of resize, layout, or ' || debugDoingThisResize
'permitted parent access. RenderBox can always access its own size, ' || debugDoingThisLayout
'otherwise, the only object that is allowed to read RenderBox.size ' || _computingThisDryLayout
'is its parent, if they have said they will. If you hit this assert ' || RenderObject.debugActiveLayout == parent && size._canBeUsedByParent;
'trying to access a child\'s size, pass "parentUsesSize: true" to ' assert(sizeAccessAllowed,
"that child's layout().", 'RenderBox.size accessed beyond the scope of resize, layout, or '
); 'permitted parent access. RenderBox can always access its own size, '
} 'otherwise, the only object that is allowed to read RenderBox.size '
'is its parent, if they have said they will. It you hit this assert '
'trying to access a child\'s size, pass "parentUsesSize: true" to '
"that child's layout() in ${objectRuntimeType(this, 'RenderBox')}.performLayout.",
);
final RenderBox? renderBoxDoingDryBaseline = _computingThisDryBaseline
? this
: (parent is RenderBox && parent._computingThisDryBaseline ? parent : null);
assert(renderBoxDoingDryBaseline == null,
'RenderBox.size accessed in '
'${objectRuntimeType(renderBoxDoingDryBaseline, 'RenderBox')}.computeDryBaseline.'
'The computeDryBaseline method must not access '
'${renderBoxDoingDryBaseline == this ? "the RenderBox's own size" : "the size of its child"},'
"because it's established in peformLayout or peformResize using different BoxConstraints."
);
assert(size == _size); assert(size == _size);
} }
return true; return true;
@ -2122,7 +2317,6 @@ abstract class RenderBox extends RenderObject {
size = size; // ignore: no_self_assignments size = size; // ignore: no_self_assignments
} }
Map<TextBaseline, double?>? _cachedBaselines;
static bool _debugDoingBaseline = false; static bool _debugDoingBaseline = false;
static bool _debugSetDoingBaseline(bool value) { static bool _debugSetDoingBaseline(bool value) {
_debugDoingBaseline = value; _debugDoingBaseline = value;
@ -2145,19 +2339,19 @@ abstract class RenderBox extends RenderObject {
/// ///
/// When implementing a [RenderBox] subclass, to override the baseline /// When implementing a [RenderBox] subclass, to override the baseline
/// computation, override [computeDistanceToActualBaseline]. /// computation, override [computeDistanceToActualBaseline].
///
/// See also:
///
/// * [getDryBaseline], which returns the baseline location of this
/// [RenderBox] at a certain [BoxConstraints].
double? getDistanceToBaseline(TextBaseline baseline, { bool onlyReal = false }) { double? getDistanceToBaseline(TextBaseline baseline, { bool onlyReal = false }) {
assert(!_debugDoingBaseline, 'Please see the documentation for computeDistanceToActualBaseline for the required calling conventions of this method.'); assert(!_debugDoingBaseline, 'Please see the documentation for computeDistanceToActualBaseline for the required calling conventions of this method.');
assert(!debugNeedsLayout); assert(!debugNeedsLayout || RenderObject.debugCheckingIntrinsics);
assert(() { assert(RenderObject.debugCheckingIntrinsics || switch (owner!) {
if (owner!.debugDoingLayout) { PipelineOwner(debugDoingLayout: true) => RenderObject.debugActiveLayout == parent && parent!.debugDoingThisLayout,
return (RenderObject.debugActiveLayout == parent) && parent!.debugDoingThisLayout; PipelineOwner(debugDoingPaint: true) => RenderObject.debugActivePaint == parent && parent!.debugDoingThisPaint || (RenderObject.debugActivePaint == this && debugDoingThisPaint),
} PipelineOwner() => false,
if (owner!.debugDoingPaint) { });
return ((RenderObject.debugActivePaint == parent) && parent!.debugDoingThisPaint) ||
((RenderObject.debugActivePaint == this) && debugDoingThisPaint);
}
return false;
}());
assert(_debugSetDoingBaseline(true)); assert(_debugSetDoingBaseline(true));
final double? result; final double? result;
try { try {
@ -2180,8 +2374,11 @@ abstract class RenderBox extends RenderObject {
@mustCallSuper @mustCallSuper
double? getDistanceToActualBaseline(TextBaseline baseline) { double? getDistanceToActualBaseline(TextBaseline baseline) {
assert(_debugDoingBaseline, 'Please see the documentation for computeDistanceToActualBaseline for the required calling conventions of this method.'); assert(_debugDoingBaseline, 'Please see the documentation for computeDistanceToActualBaseline for the required calling conventions of this method.');
_cachedBaselines ??= <TextBaseline, double?>{}; return _computeIntrinsics(
return _cachedBaselines!.putIfAbsent(baseline, () => computeDistanceToActualBaseline(baseline)); _CachedLayoutCalculation.baseline,
(constraints, baseline),
((BoxConstraints, TextBaseline) pair) => BaselineOffset(computeDistanceToActualBaseline(pair.$2)),
).offset;
} }
/// Returns the distance from the y-coordinate of the position of the box to /// Returns the distance from the y-coordinate of the position of the box to
@ -2331,7 +2528,7 @@ abstract class RenderBox extends RenderObject {
} }
// Checking that getDryLayout computes the same size. // Checking that getDryLayout computes the same size.
_dryLayoutCalculationValid = true; _debugDryLayoutCalculationValid = true;
RenderObject.debugCheckingIntrinsics = true; RenderObject.debugCheckingIntrinsics = true;
final Size dryLayoutSize; final Size dryLayoutSize;
try { try {
@ -2339,7 +2536,7 @@ abstract class RenderBox extends RenderObject {
} finally { } finally {
RenderObject.debugCheckingIntrinsics = false; RenderObject.debugCheckingIntrinsics = false;
} }
if (_dryLayoutCalculationValid && dryLayoutSize != size) { if (_debugDryLayoutCalculationValid && dryLayoutSize != size) {
throw FlutterError.fromParts(<DiagnosticsNode>[ throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The size given to the ${objectRuntimeType(this, 'RenderBox')} class differs from the size computed by computeDryLayout.'), ErrorSummary('The size given to the ${objectRuntimeType(this, 'RenderBox')} class differs from the size computed by computeDryLayout.'),
ErrorDescription( ErrorDescription(
@ -2360,42 +2557,99 @@ abstract class RenderBox extends RenderObject {
}()); }());
} }
bool _clearCachedData() { void _debugVerifyDryBaselines() {
if ((_cachedBaselines != null && _cachedBaselines!.isNotEmpty) || assert(() {
(_cachedIntrinsicDimensions != null && _cachedIntrinsicDimensions!.isNotEmpty) || final List<DiagnosticsNode> messages = <DiagnosticsNode>[
(_cachedDryLayoutSizes != null && _cachedDryLayoutSizes!.isNotEmpty)) { ErrorDescription(
// If we have cached data, then someone must have used our data. 'The constraints used were $constraints.',
// 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, ErrorHint(
// then we'll fill the cache again, and if we get dirty again, we'll 'If you are not writing your own RenderBox subclass, then this is not\n'
// notify them again. 'your fault. Contact support: https://github.com/flutter/flutter/issues/new?template=2_bug.yml',
_cachedBaselines?.clear(); )
_cachedIntrinsicDimensions?.clear(); ];
_cachedDryLayoutSizes?.clear();
for (final TextBaseline baseline in TextBaseline.values) {
assert(!RenderObject.debugCheckingIntrinsics);
RenderObject.debugCheckingIntrinsics = true;
_debugDryLayoutCalculationValid = true;
final double? dryBaseline;
final double? realBaseline;
try {
dryBaseline = getDryBaseline(constraints, baseline);
realBaseline = getDistanceToBaseline(baseline, onlyReal: true);
} finally {
RenderObject.debugCheckingIntrinsics = false;
}
assert(!RenderObject.debugCheckingIntrinsics);
if (!_debugDryLayoutCalculationValid || dryBaseline == realBaseline) {
continue;
}
if ((dryBaseline == null) != (realBaseline == null)) {
final (String methodReturnedNull, String methodReturnedNonNull) = dryBaseline == null
? ('computeDryBaseline', 'computeDistanceToActualBaseline')
: ('computeDistanceToActualBaseline', 'computeDryBaseline');
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'The $baseline location returned by ${objectRuntimeType(this, 'RenderBox')}.computeDistanceToActualBaseline '
'differs from the baseline location computed by computeDryBaseline.'
),
ErrorDescription(
'The $methodReturnedNull method returned null while the $methodReturnedNonNull returned a non-null $baseline of ${dryBaseline ?? realBaseline}. '
'Did you forget to implement $methodReturnedNull for ${objectRuntimeType(this, 'RenderBox')}?'
),
...messages,
]);
} else {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'The $baseline location returned by ${objectRuntimeType(this, 'RenderBox')}.computeDistanceToActualBaseline '
'differs from the baseline location computed by computeDryBaseline.'
),
DiagnosticsProperty<RenderObject>(
'The RenderBox was',
this,
),
ErrorDescription(
'The computeDryBaseline method returned $dryBaseline,\n'
'while the computeDistanceToActualBaseline method returned $realBaseline.\n'
'Consider checking the implementations of the following methods on the ${objectRuntimeType(this, 'RenderBox')} class and make sure they are consistent:\n'
' * computeDistanceToActualBaseline\n'
' * computeDryBaseline\n'
' * performLayout\n'
),
...messages,
]);
}
}
return true; return true;
} }());
return false;
} }
@override @override
void markNeedsLayout() { void markNeedsLayout() {
if (_clearCachedData() && parent is RenderObject) { // If `_layoutCacheStorage.clear` returns true, then this [RenderBox]'s layout
// is used by the parent's layout algorithm (it's possible that the parent
// only used the intrinsics for paint, but there's no good way to detect that
// so we conservatively assume it's a layout dependency).
//
// 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 `_layoutCacheStorage` being non-empty
// indicates that the parent depended on this RenderBox's baseline location,
// or intrinsic sizes, and thus may need relayout, regardless of relayout
// boundaries.
//
// Some calculations may fail (dry baseline, for example). The layout
// dependency is still established, but only from the RenderBox that failed
// to compute the dry baseline to the ancestor that queried the dry baseline.
if (_layoutCacheStorage.clear() && parent != null) {
markParentNeedsLayout(); markParentNeedsLayout();
return; return;
} }
super.markNeedsLayout(); super.markNeedsLayout();
} }
@override
void layout(Constraints constraints, {bool parentUsesSize = false}) {
if (hasSize && constraints != this.constraints &&
_cachedBaselines != null && _cachedBaselines!.isNotEmpty) {
// The cached baselines data may need update if the constraints change.
_cachedBaselines?.clear();
}
super.layout(constraints, parentUsesSize: parentUsesSize);
}
/// {@macro flutter.rendering.RenderObject.performResize} /// {@macro flutter.rendering.RenderObject.performResize}
/// ///
/// By default this method sets [size] to the result of [computeDryLayout] /// By default this method sets [size] to the result of [computeDryLayout]
@ -2710,6 +2964,22 @@ abstract class RenderBox extends RenderObject {
@override @override
void debugPaint(PaintingContext context, Offset offset) { void debugPaint(PaintingContext context, Offset offset) {
assert(() { assert(() {
// Only perform the baseline checks after `PipelineOwner.flushLayout` completes.
// We can't run this check in the same places we run other intrinsics checks
// (in the `RenderBox.size` setter, or after `performResize`), because
// `getDistanceToBaseline` may depend on the layout of the child so it's
// the safest to only call `getDistanceToBaseline` after the entire tree
// finishes doing layout.
//
// Descendant `RenderObject`s typically call `debugPaint` before their
// parents do. This means the baseline implementations are checked from
// descendants to ancestors, allowing us to spot the `RenderBox` with an
// inconsistent implementation, instead of its ancestors that only reported
// inconsistent baseline values because one of its ancestors has an
// inconsistent implementation.
if (debugCheckIntrinsicSizes) {
_debugVerifyDryBaselines();
}
if (debugPaintSizeEnabled) { if (debugPaintSizeEnabled) {
debugPaintSize(context, offset); debugPaintSize(context, offset);
} }
@ -2812,12 +3082,12 @@ mixin RenderBoxContainerDefaultsMixin<ChildType extends RenderBox, ParentDataTyp
assert(!debugNeedsLayout); assert(!debugNeedsLayout);
ChildType? child = firstChild; ChildType? child = firstChild;
while (child != null) { while (child != null) {
final ParentDataType? childParentData = child.parentData as ParentDataType?; final ParentDataType childParentData = child.parentData! as ParentDataType;
final double? result = child.getDistanceToActualBaseline(baseline); final double? result = child.getDistanceToActualBaseline(baseline);
if (result != null) { if (result != null) {
return result + childParentData!.offset.dy; return result + childParentData.offset.dy;
} }
child = childParentData!.nextSibling; child = childParentData.nextSibling;
} }
return null; return null;
} }
@ -2828,22 +3098,15 @@ mixin RenderBoxContainerDefaultsMixin<ChildType extends RenderBox, ParentDataTyp
/// order in the child list. /// order in the child list.
double? defaultComputeDistanceToHighestActualBaseline(TextBaseline baseline) { double? defaultComputeDistanceToHighestActualBaseline(TextBaseline baseline) {
assert(!debugNeedsLayout); assert(!debugNeedsLayout);
double? result; BaselineOffset minBaseline = BaselineOffset.noBaseline;
ChildType? child = firstChild; ChildType? child = firstChild;
while (child != null) { while (child != null) {
final ParentDataType childParentData = child.parentData! as ParentDataType; final ParentDataType childParentData = child.parentData! as ParentDataType;
double? candidate = child.getDistanceToActualBaseline(baseline); final BaselineOffset candidate = BaselineOffset(child.getDistanceToActualBaseline(baseline)) + childParentData.offset.dy;
if (candidate != null) { minBaseline = minBaseline.minOf(candidate);
candidate += childParentData.offset.dy;
if (result != null) {
result = math.min(result, candidate);
} else {
result = candidate;
}
}
child = childParentData.nextSibling; child = childParentData.nextSibling;
} }
return result; return minBaseline.offset;
} }
/// Performs a hit test on each child by walking the child list backwards. /// Performs a hit test on each child by walking the child list backwards.

View file

@ -2213,16 +2213,16 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
@protected @protected
void debugAssertDoesMeetConstraints(); void debugAssertDoesMeetConstraints();
/// When true, debugAssertDoesMeetConstraints() is currently /// When true, a debug method ([debugAssertDoesMeetConstraints], for instance)
/// executing asserts for verifying the consistent behavior of /// is currently executing asserts for verifying the consistent behavior of
/// intrinsic dimensions methods. /// intrinsic dimensions methods.
/// ///
/// This should only be set by debugAssertDoesMeetConstraints() /// This is typically set by framework debug methods. It is read by tests to
/// implementations. It is used by tests to selectively ignore /// selectively ignore custom layout callbacks. It should not be set outside of
/// custom layout callbacks. It should not be set outside of /// intrinsic-checking debug methods, and should not be checked in release mode
/// debugAssertDoesMeetConstraints(), and should not be checked in /// (where it will always be false).
/// release mode (where it will always be false).
static bool debugCheckingIntrinsics = false; static bool debugCheckingIntrinsics = false;
bool _debugSubtreeRelayoutRootAlreadyMarkedNeedsLayout() { bool _debugSubtreeRelayoutRootAlreadyMarkedNeedsLayout() {
if (_relayoutBoundary == null) { if (_relayoutBoundary == null) {
// We don't know where our relayout boundary is yet. // We don't know where our relayout boundary is yet.

View file

@ -700,8 +700,9 @@ class RenderIndexedStack extends RenderStack {
@override @override
void visitChildrenForSemantics(RenderObjectVisitor visitor) { void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (index != null && firstChild != null) { final RenderBox? displayedChild = _childAtIndex();
visitor(_childAtIndex()); if (displayedChild != null) {
visitor(displayedChild);
} }
} }
@ -715,45 +716,55 @@ class RenderIndexedStack extends RenderStack {
} }
} }
RenderBox _childAtIndex() { RenderBox? _childAtIndex() {
assert(index != null); final int? index = this.index;
RenderBox? child = firstChild; if (index == null) {
int i = 0; return null;
while (child != null && i < index!) {
final StackParentData childParentData = child.parentData! as StackParentData;
child = childParentData.nextSibling;
i += 1;
} }
assert(i == index); RenderBox? child = firstChild;
assert(child != null); for (int i = 0; i < index && child != null; i += 1) {
return child!; child = childAfter(child);
}
assert(firstChild == null || child != null);
return child;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
final RenderBox? displayedChild = _childAtIndex();
if (displayedChild == null) {
return null;
}
final StackParentData childParentData = displayedChild.parentData! as StackParentData;
final BaselineOffset offset = BaselineOffset(displayedChild.getDistanceToActualBaseline(baseline)) + childParentData.offset.dy;
return offset.offset;
} }
@override @override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
if (firstChild == null || index == null) { final RenderBox? displayedChild = _childAtIndex();
if (displayedChild == null) {
return false; return false;
} }
final RenderBox child = _childAtIndex(); final StackParentData childParentData = displayedChild.parentData! as StackParentData;
final StackParentData childParentData = child.parentData! as StackParentData;
return result.addWithPaintOffset( return result.addWithPaintOffset(
offset: childParentData.offset, offset: childParentData.offset,
position: position, position: position,
hitTest: (BoxHitTestResult result, Offset transformed) { hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset); assert(transformed == position - childParentData.offset);
return child.hitTest(result, position: transformed); return displayedChild.hitTest(result, position: transformed);
}, },
); );
} }
@override @override
void paintStack(PaintingContext context, Offset offset) { void paintStack(PaintingContext context, Offset offset) {
if (firstChild == null || index == null) { final RenderBox? displayedChild = _childAtIndex();
if (displayedChild == null) {
return; return;
} }
final RenderBox child = _childAtIndex(); final StackParentData childParentData = displayedChild.parentData! as StackParentData;
final StackParentData childParentData = child.parentData! as StackParentData; context.paintChild(displayedChild, childParentData.offset + offset);
context.paintChild(child, childParentData.offset + offset);
} }
@override @override

View file

@ -10,11 +10,6 @@ import 'package:flutter_test/flutter_test.dart';
const List<String> menuItems = <String>['one', 'two', 'three', 'four']; const List<String> menuItems = <String>['one', 'two', 'three', 'four'];
void onChanged<T>(T _) { } void onChanged<T>(T _) { }
final Type dropdownButtonType = DropdownButton<String>(
onChanged: (_) { },
items: const <DropdownMenuItem<String>>[],
).runtimeType;
Finder _iconRichText(Key iconKey) { Finder _iconRichText(Key iconKey) {
return find.descendant( return find.descendant(
of: find.byKey(iconKey), of: find.byKey(iconKey),
@ -566,8 +561,7 @@ void main() {
} }
}); });
testWidgets('DropdownButtonFormField with isDense:true does not clip large scale text', testWidgets('DropdownButtonFormField with isDense:true does not clip large scale text', (WidgetTester tester) async {
(WidgetTester tester) async {
final Key buttonKey = UniqueKey(); final Key buttonKey = UniqueKey();
const String value = 'two'; const String value = 'two';
@ -588,9 +582,11 @@ void main() {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
key: ValueKey<String>(item), key: ValueKey<String>(item),
value: item, value: item,
child: Text(item, child: Text(
key: ValueKey<String>('${item}Text'), item,
style: const TextStyle(fontSize: 20.0)), key: ValueKey<String>('${item}Text'),
style: const TextStyle(fontSize: 20.0),
),
); );
}).toList(), }).toList(),
), ),
@ -601,8 +597,7 @@ void main() {
), ),
); );
final RenderBox box = final RenderBox box = tester.renderObject<RenderBox>(find.byType(DropdownButton<String>));
tester.renderObject<RenderBox>(find.byType(dropdownButtonType));
expect(box.size.height, 64.0); expect(box.size.height, 64.0);
}); });
@ -633,7 +628,7 @@ void main() {
), ),
); );
final RenderBox box = tester.renderObject<RenderBox>(find.byType(dropdownButtonType)); final RenderBox box = tester.renderObject<RenderBox>(find.byType(DropdownButton<String>));
expect(box.size.height, 48.0); expect(box.size.height, 48.0);
}); });
@ -1077,7 +1072,7 @@ void main() {
expect(find.text(currentValue), findsOneWidget); expect(find.text(currentValue), findsOneWidget);
// Tap the DropdownButtonFormField widget // Tap the DropdownButtonFormField widget
await tester.tap(find.byType(dropdownButtonType)); await tester.tap(find.byType(DropdownButton<String>));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Tap the first dropdown menu item. // Tap the first dropdown menu item.

View file

@ -1274,7 +1274,7 @@ void main() {
}); });
testWidgets('Material2 - ToggleButtons text baseline alignment', (WidgetTester tester) async { testWidgets('Material2 - ToggleButtons text baseline alignment', (WidgetTester tester) async {
// The point size of the fonts must be a multiple of 4 until // The font size must be a multiple of 4 until
// https://github.com/flutter/flutter/issues/122066 is resolved. // https://github.com/flutter/flutter/issues/122066 is resolved.
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(

View file

@ -1227,6 +1227,23 @@ void main() {
); );
layout(goodRoot, onErrors: () { assert(false); }); layout(goodRoot, onErrors: () { assert(false); });
}); });
group('BaselineOffset', () {
test('minOf', () {
expect(BaselineOffset.noBaseline.minOf(BaselineOffset.noBaseline), BaselineOffset.noBaseline);
expect(BaselineOffset.noBaseline.minOf(const BaselineOffset(1)), const BaselineOffset(1));
expect(const BaselineOffset(1).minOf(BaselineOffset.noBaseline), const BaselineOffset(1));
expect(const BaselineOffset(2).minOf(const BaselineOffset(1)), const BaselineOffset(1));
expect(const BaselineOffset(1).minOf(const BaselineOffset(2)), const BaselineOffset(1));
});
test('+', () {
expect(BaselineOffset.noBaseline + 2, BaselineOffset.noBaseline);
expect(const BaselineOffset(1) + 2, const BaselineOffset(3));
});
});
} }
class _DummyHitTestTarget implements HitTestTarget { class _DummyHitTestTarget implements HitTestTarget {

View file

@ -37,6 +37,34 @@ class RenderTestBox extends RenderBox {
} }
} }
class RenderDryBaselineTestBox extends RenderTestBox {
double? baselineOverride;
@override
double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
calls += 1;
return baselineOverride ?? constraints.biggest.height / 2.0;
}
}
class RenderBadDryBaselineTestBox extends RenderTestBox {
@override
double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
return size.height / 2.0;
}
}
class RenderCannotComputeDryBaselineTestBox extends RenderTestBox {
bool shouldAssert = true;
@override
double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
if (shouldAssert) {
assert(debugCannotComputeDryLayout(reason: 'no dry baseline for you'));
}
return null;
}
}
void main() { void main() {
TestRenderingFlutterBinding.ensureInitialized(); TestRenderingFlutterBinding.ensureInitialized();
@ -134,4 +162,99 @@ void main() {
expect(test.calls, 3); // Use the cached data if the layout constraints do not change. expect(test.calls, 3); // Use the cached data if the layout constraints do not change.
}); });
group('Dry baseline', () {
test('computeDryBaseline results are cached and shared with computeDistanceToActualBaseline', () {
const double viewHeight = 200.0;
const BoxConstraints constraints = BoxConstraints.tightFor(width: 200.0, height: viewHeight);
final RenderDryBaselineTestBox test = RenderDryBaselineTestBox();
final RenderBox baseline = RenderBaseline(
baseline: 0.0,
baselineType: TextBaseline.alphabetic,
child: test,
);
final RenderConstrainedBox root = RenderConstrainedBox(
additionalConstraints: constraints,
child: baseline,
);
layout(RenderPositionedBox(child: root));
expect(test.calls, 1);
// The baseline widget loosens the input constraints when passing on to child.
expect(test.getDryBaseline(constraints.loosen(), TextBaseline.alphabetic), test.boxSize.height / 2);
// There's cache for the constraints so this should be 1, but we always evaluate
// computeDryBaseline in debug mode in case it asserts even if the baseline
// cache hits.
expect(test.calls, 2);
const BoxConstraints newConstraints = BoxConstraints.tightFor(width: 10.0, height: 10.0);
expect(test.getDryBaseline(newConstraints.loosen(), TextBaseline.alphabetic), 5.0);
// Should be 3 but there's an additional computeDryBaseline call in getDryBaseline,
// in an assert.
expect(test.calls, 4);
root.additionalConstraints = newConstraints;
pumpFrame();
expect(test.calls, 4);
});
test('Asserts when a RenderBox cannot compute dry baseline', () {
final RenderCannotComputeDryBaselineTestBox test = RenderCannotComputeDryBaselineTestBox();
layout(RenderBaseline(baseline: 0.0, baselineType: TextBaseline.alphabetic, child: test));
final BoxConstraints incomingConstraints = test.constraints;
assert(incomingConstraints != const BoxConstraints());
expect(
() => test.getDryBaseline(const BoxConstraints(), TextBaseline.alphabetic),
throwsA(isA<AssertionError>().having((AssertionError e) => e.message, 'message', contains('no dry baseline for you'))),
);
// Still throws when there is cache.
expect(
() => test.getDryBaseline(incomingConstraints, TextBaseline.alphabetic),
throwsA(isA<AssertionError>().having((AssertionError e) => e.message, 'message', contains('no dry baseline for you'))),
);
});
test('Cactches inconsistencies between computeDryBaseline and computeDistanceToActualBaseline', () {
final RenderDryBaselineTestBox test = RenderDryBaselineTestBox();
layout(test, phase: EnginePhase.composite);
FlutterErrorDetails? error;
test.markNeedsLayout();
test.baselineOverride = 123;
pumpFrame(phase: EnginePhase.composite, onErrors: () {
error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails();
});
expect(error?.exceptionAsString(), contains('differs from the baseline location computed by computeDryBaseline'));
});
test('Accessing RenderBox.size in computeDryBaseline is not allowed', () {
final RenderBadDryBaselineTestBox test = RenderBadDryBaselineTestBox();
FlutterErrorDetails? error;
layout(test, phase: EnginePhase.composite, onErrors: () {
error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails();
});
expect(error?.exceptionAsString(), contains('RenderBox.size accessed in RenderBadDryBaselineTestBox.computeDryBaseline.'));
});
test('debug baseline checks do not freak out when debugCannotComputeDryLayout is called', () {
FlutterErrorDetails? error;
void onErrors() {
error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails();
}
final RenderCannotComputeDryBaselineTestBox test = RenderCannotComputeDryBaselineTestBox();
layout(test, phase: EnginePhase.composite, onErrors: onErrors);
expect(error, isNull);
test.shouldAssert = false;
test.markNeedsLayout();
pumpFrame(phase: EnginePhase.composite, onErrors: onErrors);
expect(error?.exceptionAsString(), contains('differs from the baseline location computed by computeDryBaseline'));
});
});
} }

View file

@ -119,6 +119,7 @@ void main() {
visitedChildren.add(child); visitedChildren.add(child);
} }
layout(stack);
stack.visitChildrenForSemantics(visitor); stack.visitChildrenForSemantics(visitor);
expect(visitedChildren, hasLength(1)); expect(visitedChildren, hasLength(1));

View file

@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
@ -44,6 +45,8 @@ void main() {
}); });
testWidgets('Chip caches baseline', (WidgetTester tester) async { testWidgets('Chip caches baseline', (WidgetTester tester) async {
final bool checkIntrinsicSizes = debugCheckIntrinsicSizes;
debugCheckIntrinsicSizes = false;
int calls = 0; int calls = 0;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@ -53,6 +56,7 @@ void main() {
baselineType: TextBaseline.alphabetic, baselineType: TextBaseline.alphabetic,
child: Chip( child: Chip(
label: BaselineDetector(() { label: BaselineDetector(() {
assert(!debugCheckIntrinsicSizes);
calls += 1; calls += 1;
}), }),
), ),
@ -66,9 +70,12 @@ void main() {
tester.renderObject<RenderBaselineDetector>(find.byType(BaselineDetector)).dirty(); tester.renderObject<RenderBaselineDetector>(find.byType(BaselineDetector)).dirty();
await tester.pump(); await tester.pump();
expect(calls, 2); expect(calls, 2);
debugCheckIntrinsicSizes = checkIntrinsicSizes;
}); });
testWidgets('ListTile caches baseline', (WidgetTester tester) async { testWidgets('ListTile caches baseline', (WidgetTester tester) async {
final bool checkIntrinsicSizes = debugCheckIntrinsicSizes;
debugCheckIntrinsicSizes = false;
int calls = 0; int calls = 0;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@ -78,6 +85,7 @@ void main() {
baselineType: TextBaseline.alphabetic, baselineType: TextBaseline.alphabetic,
child: ListTile( child: ListTile(
title: BaselineDetector(() { title: BaselineDetector(() {
assert(!debugCheckIntrinsicSizes);
calls += 1; calls += 1;
}), }),
), ),
@ -91,6 +99,7 @@ void main() {
tester.renderObject<RenderBaselineDetector>(find.byType(BaselineDetector)).dirty(); tester.renderObject<RenderBaselineDetector>(find.byType(BaselineDetector)).dirty();
await tester.pump(); await tester.pump();
expect(calls, 2); expect(calls, 2);
debugCheckIntrinsicSizes = checkIntrinsicSizes;
}); });
testWidgets("LayoutBuilder returns child's baseline", (WidgetTester tester) async { testWidgets("LayoutBuilder returns child's baseline", (WidgetTester tester) async {