Prepare for RenderDecorator.computeBaseline changes. (#146363)

Minor changes to make the `RenderDecorator.computeBaseline` change a bit easier to make. No semantic changes.
This commit is contained in:
LongCatIsLooong 2024-04-06 14:51:23 -07:00 committed by GitHub
parent 2c038e6cbb
commit 98d23f709f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 259 additions and 377 deletions

View File

@ -30,6 +30,9 @@ const Duration _kTransitionDuration = Duration(milliseconds: 167);
const Curve _kTransitionCurve = Curves.fastOutSlowIn; const Curve _kTransitionCurve = Curves.fastOutSlowIn;
const double _kFinalLabelScale = 0.75; const double _kFinalLabelScale = 0.75;
typedef _SubtextSize = ({ double ascent, double bottomHeight, double subtextHeight });
typedef _ChildBaselineGetter = double Function(RenderBox child, BoxConstraints constraints);
// The default duration for hint fade in/out transitions. // The default duration for hint fade in/out transitions.
// //
// Animating hint is not mentioned in the Material specification. // Animating hint is not mentioned in the Material specification.
@ -614,7 +617,7 @@ class _Decoration {
this.container, this.container,
}); });
final EdgeInsetsGeometry contentPadding; final EdgeInsetsDirectional contentPadding;
final bool isCollapsed; final bool isCollapsed;
final double floatingLabelHeight; final double floatingLabelHeight;
final double floatingLabelProgress; final double floatingLabelProgress;
@ -698,20 +701,16 @@ class _Decoration {
// all of the renderer children of a _RenderDecoration. // all of the renderer children of a _RenderDecoration.
class _RenderDecorationLayout { class _RenderDecorationLayout {
const _RenderDecorationLayout({ const _RenderDecorationLayout({
required this.boxToBaseline, required this.baseline,
required this.inputBaseline, // for InputBorderType.underline
required this.outlineBaseline, // for InputBorderType.outline
required this.subtextBaseline,
required this.containerHeight, required this.containerHeight,
required this.subtextHeight, required this.subtextSize,
required this.size,
}); });
final Map<RenderBox?, double> boxToBaseline; final double baseline;
final double inputBaseline;
final double outlineBaseline;
final double subtextBaseline; // helper/error counter
final double containerHeight; final double containerHeight;
final double subtextHeight; final _SubtextSize? subtextSize;
final Size size;
} }
// The workhorse: layout and paint a _Decorator widget's _Decoration. // The workhorse: layout and paint a _Decorator widget's _Decoration.
@ -742,13 +741,14 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
RenderBox? get suffix => childForSlot(_DecorationSlot.suffix); RenderBox? get suffix => childForSlot(_DecorationSlot.suffix);
RenderBox? get prefixIcon => childForSlot(_DecorationSlot.prefixIcon); RenderBox? get prefixIcon => childForSlot(_DecorationSlot.prefixIcon);
RenderBox? get suffixIcon => childForSlot(_DecorationSlot.suffixIcon); RenderBox? get suffixIcon => childForSlot(_DecorationSlot.suffixIcon);
RenderBox? get helperError => childForSlot(_DecorationSlot.helperError); RenderBox get helperError => childForSlot(_DecorationSlot.helperError)!;
RenderBox? get counter => childForSlot(_DecorationSlot.counter); RenderBox? get counter => childForSlot(_DecorationSlot.counter);
RenderBox? get container => childForSlot(_DecorationSlot.container); RenderBox? get container => childForSlot(_DecorationSlot.container);
// The returned list is ordered for hit testing. // The returned list is ordered for hit testing.
@override @override
Iterable<RenderBox> get children { Iterable<RenderBox> get children {
final RenderBox? helperError = childForSlot(_DecorationSlot.helperError);
return <RenderBox>[ return <RenderBox>[
if (icon != null) if (icon != null)
icon!, icon!,
@ -767,7 +767,7 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
if (hint != null) if (hint != null)
hint!, hint!,
if (helperError != null) if (helperError != null)
helperError!, helperError,
if (counter != null) if (counter != null)
counter!, counter!,
if (container != null) if (container != null)
@ -894,70 +894,62 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
if (container != null) { if (container != null) {
visitor(container!); visitor(container!);
} }
if (helperError != null) { visitor(helperError);
visitor(helperError!);
}
if (counter != null) { if (counter != null) {
visitor(counter!); visitor(counter!);
} }
} }
@override static double _minWidth(RenderBox? box, double height) => box?.getMinIntrinsicWidth(height) ?? 0.0;
bool get sizedByParent => false; static double _maxWidth(RenderBox? box, double height) => box?.getMaxIntrinsicWidth(height) ?? 0.0 ;
static double _minHeight(RenderBox? box, double width) => box?.getMinIntrinsicHeight(width) ?? 0.0;
static double _minWidth(RenderBox? box, double height) { static Size _boxSize(RenderBox? box) => box?.size ?? Size.zero;
return box == null ? 0.0 : box.getMinIntrinsicWidth(height); static double _getBaseline(RenderBox box, BoxConstraints boxConstraints) {
return ChildLayoutHelper.getBaseline(box, boxConstraints, TextBaseline.alphabetic) ?? box.size.height;
} }
static double _maxWidth(RenderBox? box, double height) {
return box == null ? 0.0 : box.getMaxIntrinsicWidth(height);
}
static double _minHeight(RenderBox? box, double width) {
return box == null ? 0.0 : box.getMinIntrinsicHeight(width);
}
static Size _boxSize(RenderBox? box) => box == null ? Size.zero : box.size;
static BoxParentData _boxParentData(RenderBox box) => box.parentData! as BoxParentData; static BoxParentData _boxParentData(RenderBox box) => box.parentData! as BoxParentData;
EdgeInsets get contentPadding => decoration.contentPadding as EdgeInsets; EdgeInsetsDirectional get contentPadding => decoration.contentPadding;
// Lay out the given box if needed, and return its baseline. _SubtextSize? _computeSubtextSizes({
double _layoutLineBox(RenderBox? box, BoxConstraints constraints) { required BoxConstraints constraints,
if (box == null) { required ChildLayouter layoutChild,
return 0.0; required _ChildBaselineGetter getBaseline,
}) {
final RenderBox? counter = this.counter;
Size counterSize;
final double counterAscent;
if (counter != null) {
counterSize = layoutChild(counter, constraints);
counterAscent = getBaseline(counter, constraints);
} else {
counterSize = Size.zero;
counterAscent = 0.0;
} }
box.layout(constraints, parentUsesSize: true);
// Since internally, all layout is performed against the alphabetic baseline,
// (eg, ascents/descents are all relative to alphabetic, even if the font is
// an ideographic or hanging font), we should always obtain the reference
// baseline from the alphabetic baseline. The ideographic baseline is for
// use post-layout and is derived from the alphabetic baseline combined with
// the font metrics.
final double baseline = box.getDistanceToBaseline(TextBaseline.alphabetic)!;
assert(() { final BoxConstraints helperErrorConstraints = constraints.deflate(EdgeInsets.only(left: counterSize.width));
if (baseline >= 0) { final double helperErrorHeight = layoutChild(helperError, helperErrorConstraints).height;
return true;
} if (helperErrorHeight == 0.0 && counterSize.height == 0.0) {
throw FlutterError.fromParts(<DiagnosticsNode>[ return null;
ErrorSummary("One of InputDecorator's children reported a negative baseline offset."), }
ErrorDescription(
'${box.runtimeType}, of size ${box.size}, reported a negative ' // TODO(LongCatIsLooong): the bottomHeight expression doesn't make much sense.
'alphabetic baseline of $baseline.', // Use the real descent and make sure the subtext line box is tall enough for both children.
), // See https://github.com/flutter/flutter/issues/13715
]); final double ascent = math.max(counterAscent, getBaseline(helperError, helperErrorConstraints)) + subtextGap;
}()); final double bottomHeight = math.max(counterAscent, helperErrorHeight) + subtextGap;
return baseline; final double subtextHeight = math.max(counterSize.height, helperErrorHeight) + subtextGap;
return (ascent: ascent, bottomHeight: bottomHeight, subtextHeight: subtextHeight);
} }
// Returns a value used by performLayout to position all of the renderers. // Returns a value used by performLayout to position all of the renderers.
// This method applies layout to all of the renderers except the container. // This method applies layout to all of the renderers except the container.
// For convenience, the container is laid out in performLayout(). // For convenience, the container is laid out in performLayout().
_RenderDecorationLayout _layout(BoxConstraints layoutConstraints) { _RenderDecorationLayout _layout(BoxConstraints constraints) {
assert( assert(
layoutConstraints.maxWidth < double.infinity, constraints.maxWidth < double.infinity,
'An InputDecorator, which is typically created by a TextField, cannot ' 'An InputDecorator, which is typically created by a TextField, cannot '
'have an unbounded width.\n' 'have an unbounded width.\n'
'This happens when the parent widget does not provide a finite width ' 'This happens when the parent widget does not provide a finite width '
@ -967,122 +959,82 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
'TextField that contains it.', 'TextField that contains it.',
); );
// Margin on each side of subtext (counter and helperError) final BoxConstraints boxConstraints = constraints.loosen();
final Map<RenderBox?, double> boxToBaseline = <RenderBox?, double>{};
final BoxConstraints boxConstraints = layoutConstraints.loosen();
// Layout all the widgets used by InputDecorator // Layout all the widgets used by InputDecorator
boxToBaseline[icon] = _layoutLineBox(icon, boxConstraints); final double iconWidth = (icon?..layout(boxConstraints, parentUsesSize: true))?.size.width ?? 0.0;
final BoxConstraints containerConstraints = boxConstraints.copyWith( final BoxConstraints containerConstraints = boxConstraints.deflate(EdgeInsets.only(left: iconWidth));
maxWidth: boxConstraints.maxWidth - _boxSize(icon).width, final BoxConstraints contentConstraints = containerConstraints.deflate(EdgeInsets.only(left: contentPadding.horizontal));
);
boxToBaseline[prefixIcon] = _layoutLineBox(prefixIcon, containerConstraints);
boxToBaseline[suffixIcon] = _layoutLineBox(suffixIcon, containerConstraints);
final BoxConstraints contentConstraints = containerConstraints.copyWith(
maxWidth: math.max(0.0, containerConstraints.maxWidth - contentPadding.horizontal),
);
boxToBaseline[prefix] = _layoutLineBox(prefix, contentConstraints);
boxToBaseline[suffix] = _layoutLineBox(suffix, contentConstraints);
final double inputWidth = math.max(
0.0,
constraints.maxWidth - (
_boxSize(icon).width
+ (prefixIcon != null ? 0 : (textDirection == TextDirection.ltr ? contentPadding.left : contentPadding.right))
+ _boxSize(prefixIcon).width
+ _boxSize(prefix).width
+ _boxSize(suffix).width
+ _boxSize(suffixIcon).width
+ (suffixIcon != null ? 0 : (textDirection == TextDirection.ltr ? contentPadding.right : contentPadding.left))),
);
// Increase the available width for the label when it is scaled down.
final double invertedLabelScale = lerpDouble(1.00, 1 / _kFinalLabelScale, decoration.floatingLabelProgress)!;
double suffixIconWidth = _boxSize(suffixIcon).width;
if (decoration.border.isOutline) {
suffixIconWidth = lerpDouble(suffixIconWidth, 0.0, decoration.floatingLabelProgress)!;
}
final double labelWidth = math.max(
0.0,
constraints.maxWidth - (
_boxSize(icon).width
+ contentPadding.left
+ _boxSize(prefixIcon).width
+ suffixIconWidth
+ contentPadding.right),
);
boxToBaseline[label] = _layoutLineBox(
label,
boxConstraints.copyWith(maxWidth: labelWidth * invertedLabelScale),
);
boxToBaseline[hint] = _layoutLineBox(
hint,
boxConstraints.copyWith(minWidth: inputWidth, maxWidth: inputWidth),
);
boxToBaseline[counter] = _layoutLineBox(counter, contentConstraints);
// The helper or error text can occupy the full width less the space // The helper or error text can occupy the full width less the space
// occupied by the icon and counter. // occupied by the icon and counter.
boxToBaseline[helperError] = _layoutLineBox( final _SubtextSize? subtextSize = _computeSubtextSizes(
helperError, constraints: contentConstraints,
contentConstraints.copyWith( layoutChild: ChildLayoutHelper.layoutChild,
maxWidth: math.max(0.0, contentConstraints.maxWidth - _boxSize(counter).width), getBaseline: _getBaseline,
),
); );
final RenderBox? prefixIcon = this.prefixIcon;
final RenderBox? suffixIcon = this.suffixIcon;
final Size prefixIconSize = (prefixIcon?..layout(containerConstraints, parentUsesSize: true))?.size ?? Size.zero;
final Size suffixIconSize = (suffixIcon?..layout(containerConstraints, parentUsesSize: true))?.size ?? Size.zero;
final RenderBox? prefix = this.prefix;
final RenderBox? suffix = this.suffix;
final Size prefixSize = (prefix?..layout(contentConstraints, parentUsesSize: true))?.size ?? Size.zero;
final Size suffixSize = (suffix?..layout(contentConstraints, parentUsesSize: true))?.size ?? Size.zero;
final EdgeInsetsDirectional accessoryHorizontalInsets = EdgeInsetsDirectional.only(
start: iconWidth + prefixSize.width + (prefixIcon == null ? contentPadding.start : prefixIcon.size.width),
end: suffixSize.width + (suffixIcon == null ? contentPadding.end : suffixIcon.size.width),
);
final double inputWidth = math.max(0.0, constraints.maxWidth - accessoryHorizontalInsets.horizontal);
final RenderBox? label = this.label;
if (label != null) {
final double suffixIconSpace = decoration.border.isOutline
? lerpDouble(suffixIconSize.width, 0.0, decoration.floatingLabelProgress)!
: suffixIconSize.width;
final double labelWidth = math.max(
0.0,
constraints.maxWidth - (iconWidth + contentPadding.horizontal + prefixIconSize.width + suffixIconSpace),
);
// Increase the available width for the label when it is scaled down.
final double invertedLabelScale = lerpDouble(1.00, 1 / _kFinalLabelScale, decoration.floatingLabelProgress)!;
final BoxConstraints labelConstraints = boxConstraints.copyWith(maxWidth: labelWidth * invertedLabelScale);
label.layout(labelConstraints, parentUsesSize: true);
}
// The height of the input needs to accommodate label above and counter and // The height of the input needs to accommodate label above and counter and
// helperError below, when they exist. // helperError below, when they exist.
final double labelHeight = label == null final double labelHeight = label == null ? 0 : decoration.floatingLabelHeight;
? 0
: decoration.floatingLabelHeight;
final double topHeight = decoration.border.isOutline final double topHeight = decoration.border.isOutline
? math.max(labelHeight - boxToBaseline[label]!, 0) ? math.max(labelHeight - (label?.getDistanceToBaseline(TextBaseline.alphabetic) ?? 0.0), 0.0)
: labelHeight; : labelHeight;
final double counterHeight = counter == null final double bottomHeight = subtextSize?.bottomHeight ?? 0.0;
? 0
: boxToBaseline[counter]! + subtextGap;
final bool helperErrorExists = helperError?.size != null
&& helperError!.size.height > 0;
final double helperErrorHeight = !helperErrorExists
? 0
: helperError!.size.height + subtextGap;
final double bottomHeight = math.max(
counterHeight,
helperErrorHeight,
);
final Offset densityOffset = decoration.visualDensity.baseSizeAdjustment; final Offset densityOffset = decoration.visualDensity.baseSizeAdjustment;
boxToBaseline[input] = _layoutLineBox( final BoxConstraints inputConstraints = boxConstraints
input, .deflate(EdgeInsets.only(top: contentPadding.vertical + topHeight + bottomHeight + densityOffset.dy))
boxConstraints.deflate(EdgeInsets.only( .tighten(width: inputWidth);
top: contentPadding.top + topHeight + densityOffset.dy / 2,
bottom: contentPadding.bottom + bottomHeight + densityOffset.dy / 2, final RenderBox? input = this.input;
)).copyWith( final RenderBox? hint = this.hint;
minWidth: inputWidth, final Size inputSize = (input?..layout(inputConstraints, parentUsesSize: true))?.size ?? Size.zero;
maxWidth: inputWidth, final Size hintSize = (hint?..layout(boxConstraints.tighten(width: inputWidth), parentUsesSize: true))?.size ?? Size.zero;
), final double inputBaseline = input == null ? 0.0 : _getBaseline(input, inputConstraints);
); final double hintBaseline = hint == null ? 0.0 : _getBaseline(hint, boxConstraints.tighten(width: inputWidth));
// The field can be occupied by a hint or by the input itself // The field can be occupied by a hint or by the input itself
final double hintHeight = hint?.size.height ?? 0; final double inputHeight = math.max(hintSize.height, inputSize.height);
final double inputDirectHeight = input?.size.height ?? 0; final double inputInternalBaseline = math.max(inputBaseline, hintBaseline);
final double inputHeight = math.max(hintHeight, inputDirectHeight);
final double inputInternalBaseline = math.max(
boxToBaseline[input]!,
boxToBaseline[hint]!,
);
final double prefixBaseline = prefix == null ? 0.0 : _getBaseline(prefix, contentConstraints);
final double suffixBaseline = suffix == null ? 0.0 : _getBaseline(suffix, contentConstraints);
// Calculate the amount that prefix/suffix affects height above and below // Calculate the amount that prefix/suffix affects height above and below
// the input. // the input.
final double prefixHeight = prefix?.size.height ?? 0; final double fixHeight = math.max(prefixBaseline, suffixBaseline);
final double suffixHeight = suffix?.size.height ?? 0;
final double fixHeight = math.max(
boxToBaseline[prefix]!,
boxToBaseline[suffix]!,
);
final double fixAboveInput = math.max(0, fixHeight - inputInternalBaseline); final double fixAboveInput = math.max(0, fixHeight - inputInternalBaseline);
final double fixBelowBaseline = math.max( final double fixBelowBaseline = math.max(prefixSize.height - prefixBaseline, suffixSize.height - suffixBaseline);
prefixHeight - boxToBaseline[prefix]!,
suffixHeight - boxToBaseline[suffix]!,
);
// TODO(justinmc): fixBelowInput should have no effect when there is no // TODO(justinmc): fixBelowInput should have no effect when there is no
// prefix/suffix below the input. // prefix/suffix below the input.
// https://github.com/flutter/flutter/issues/66050 // https://github.com/flutter/flutter/issues/66050
@ -1092,9 +1044,7 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
); );
// Calculate the height of the input text container. // Calculate the height of the input text container.
final double prefixIconHeight = prefixIcon?.size.height ?? 0; final double fixIconHeight = math.max(prefixIconSize.height, suffixIconSize.height);
final double suffixIconHeight = suffixIcon?.size.height ?? 0;
final double fixIconHeight = math.max(prefixIconHeight, suffixIconHeight);
final double contentHeight = math.max( final double contentHeight = math.max(
fixIconHeight, fixIconHeight,
topHeight topHeight
@ -1141,62 +1091,40 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
final double maxContentHeight = containerHeight - contentPadding.vertical - topHeight - densityOffset.dy; final double maxContentHeight = containerHeight - contentPadding.vertical - topHeight - densityOffset.dy;
final double alignableHeight = fixAboveInput + inputHeight + fixBelowInput; final double alignableHeight = fixAboveInput + inputHeight + fixBelowInput;
final double maxVerticalOffset = maxContentHeight - alignableHeight; final double maxVerticalOffset = maxContentHeight - alignableHeight;
final double textAlignVerticalOffset = maxVerticalOffset * textAlignVerticalFactor;
final double inputBaseline = topInputBaseline + textAlignVerticalOffset;
// The three main alignments for the baseline when an outline is present are final double baseline;
// if (_isOutlineAligned) {
// * top (-1.0): topmost point considering padding. // The three main alignments for the baseline when an outline is present are
// * center (0.0): the absolute center of the input ignoring padding but //
// accommodating the border and floating label. // * top (-1.0): topmost point considering padding.
// * bottom (1.0): bottommost point considering padding. // * center (0.0): the absolute center of the input ignoring padding but
// // accommodating the border and floating label.
// That means that if the padding is uneven, center is not the exact // * bottom (1.0): bottommost point considering padding.
// midpoint of top and bottom. To account for this, the above center and //
// below center alignments are interpolated independently. // That means that if the padding is uneven, center is not the exact
final double outlineCenterBaseline = inputInternalBaseline // midpoint of top and bottom. To account for this, the above center and
+ baselineAdjustment / 2.0 // below center alignments are interpolated independently.
+ (containerHeight - inputHeight) / 2.0; final double outlineCenterBaseline = inputInternalBaseline
final double outlineTopBaseline = topInputBaseline; + baselineAdjustment / 2.0
final double outlineBottomBaseline = topInputBaseline + maxVerticalOffset; + (containerHeight - inputHeight) / 2.0;
final double outlineBaseline = _interpolateThree( final double outlineTopBaseline = topInputBaseline;
outlineTopBaseline, final double outlineBottomBaseline = topInputBaseline + maxVerticalOffset;
outlineCenterBaseline, baseline = _interpolateThree(
outlineBottomBaseline, outlineTopBaseline,
textAlignVertical, outlineCenterBaseline,
); outlineBottomBaseline,
textAlignVertical,
// Find the positions of the text below the input when it exists. );
double subtextCounterBaseline = 0; } else {
double subtextHelperBaseline = 0; final double textAlignVerticalOffset = maxVerticalOffset * textAlignVerticalFactor;
double subtextCounterHeight = 0; baseline = topInputBaseline + textAlignVerticalOffset;
double subtextHelperHeight = 0;
if (counter != null) {
subtextCounterBaseline =
containerHeight + subtextGap + boxToBaseline[counter]!;
subtextCounterHeight = counter!.size.height + subtextGap;
} }
if (helperErrorExists) {
subtextHelperBaseline =
containerHeight + subtextGap + boxToBaseline[helperError]!;
subtextHelperHeight = helperErrorHeight;
}
final double subtextBaseline = math.max(
subtextCounterBaseline,
subtextHelperBaseline,
);
final double subtextHeight = math.max(
subtextCounterHeight,
subtextHelperHeight,
);
return _RenderDecorationLayout( return _RenderDecorationLayout(
boxToBaseline: boxToBaseline,
containerHeight: containerHeight, containerHeight: containerHeight,
inputBaseline: inputBaseline, baseline: baseline,
outlineBaseline: outlineBaseline, subtextSize: subtextSize,
subtextBaseline: subtextBaseline, size: Size(constraints.maxWidth, containerHeight + (subtextSize?.subtextHeight ?? 0.0)),
subtextHeight: subtextHeight,
); );
} }
@ -1207,50 +1135,37 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
// alignment is greater than zero, it interpolates between the centered box's // alignment is greater than zero, it interpolates between the centered box's
// top and the position that would align the bottom of the box with the bottom // top and the position that would align the bottom of the box with the bottom
// padding. // padding.
double _interpolateThree(double begin, double middle, double end, TextAlignVertical textAlignVertical) { static double _interpolateThree(double begin, double middle, double end, TextAlignVertical textAlignVertical) {
if (textAlignVertical.y <= 0) { // It's possible for begin, middle, and end to not be in order because of
// It's possible for begin, middle, and end to not be in order because of // excessive padding. Those cases are handled by using middle.
// excessive padding. Those cases are handled by using middle. final double basis = textAlignVertical.y <= 0
if (begin >= middle) { ? math.max(middle - begin, 0)
return middle; : math.max(end - middle, 0);
} return middle + basis * textAlignVertical.y;
// Do a standard linear interpolation on the first half, between begin and
// middle.
final double t = textAlignVertical.y + 1;
return begin + (middle - begin) * t;
}
if (middle >= end) {
return middle;
}
// Do a standard linear interpolation on the second half, between middle and
// end.
final double t = textAlignVertical.y;
return middle + (end - middle) * t;
} }
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
return _minWidth(icon, height) return _minWidth(icon, height)
+ (prefixIcon != null ? 0.0 : (textDirection == TextDirection.ltr ? contentPadding.left : contentPadding.right)) + (prefixIcon != null ? 0.0 : contentPadding.start)
+ _minWidth(prefixIcon, height) + _minWidth(prefixIcon, height)
+ _minWidth(prefix, height) + _minWidth(prefix, height)
+ math.max(_minWidth(input, height), _minWidth(hint, height)) + math.max(_minWidth(input, height), _minWidth(hint, height))
+ _minWidth(suffix, height) + _minWidth(suffix, height)
+ _minWidth(suffixIcon, height) + _minWidth(suffixIcon, height)
+ (suffixIcon != null ? 0.0 : (textDirection == TextDirection.ltr ? contentPadding.right : contentPadding.left)); + (suffixIcon != null ? 0.0 : contentPadding.end);
} }
@override @override
double computeMaxIntrinsicWidth(double height) { double computeMaxIntrinsicWidth(double height) {
return _maxWidth(icon, height) return _maxWidth(icon, height)
+ (prefixIcon != null ? 0.0 : (textDirection == TextDirection.ltr ? contentPadding.left : contentPadding.right)) + (prefixIcon != null ? 0.0 : contentPadding.start)
+ _maxWidth(prefixIcon, height) + _maxWidth(prefixIcon, height)
+ _maxWidth(prefix, height) + _maxWidth(prefix, height)
+ math.max(_maxWidth(input, height), _maxWidth(hint, height)) + math.max(_maxWidth(input, height), _maxWidth(hint, height))
+ _maxWidth(suffix, height) + _maxWidth(suffix, height)
+ _maxWidth(suffixIcon, height) + _maxWidth(suffixIcon, height)
+ (suffixIcon != null ? 0.0 : (textDirection == TextDirection.ltr ? contentPadding.right : contentPadding.left)); + (suffixIcon != null ? 0.0 : contentPadding.end);
} }
double _lineHeight(double width, List<RenderBox?> boxes) { double _lineHeight(double width, List<RenderBox?> boxes) {
@ -1282,6 +1197,8 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
width = math.max(width - contentPadding.horizontal, 0.0); width = math.max(width - contentPadding.horizontal, 0.0);
// TODO(LongCatIsLooong): use _computeSubtextSizes for subtext intrinsic sizes.
// See https://github.com/flutter/flutter/issues/13715.
final double counterHeight = _minHeight(counter, width); final double counterHeight = _minHeight(counter, width);
final double counterWidth = _minWidth(counter, counterHeight); final double counterWidth = _minWidth(counter, counterHeight);
@ -1312,6 +1229,7 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
final double minContainerHeight = decoration.isDense! || expands final double minContainerHeight = decoration.isDense! || expands
? 0.0 ? 0.0
: kMinInteractiveDimension; : kMinInteractiveDimension;
return math.max(containerHeight, minContainerHeight) + subtextHeight; return math.max(containerHeight, minContainerHeight) + subtextHeight;
} }
@ -1339,43 +1257,16 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
return Size.zero; return Size.zero;
} }
ChildSemanticsConfigurationsResult _childSemanticsConfigurationDelegate(List<SemanticsConfiguration> childConfigs) {
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
List<SemanticsConfiguration>? prefixMergeGroup;
List<SemanticsConfiguration>? suffixMergeGroup;
for (final SemanticsConfiguration childConfig in childConfigs) {
if (childConfig.tagsChildrenWith(_InputDecoratorState._kPrefixSemanticsTag)) {
prefixMergeGroup ??= <SemanticsConfiguration>[];
prefixMergeGroup.add(childConfig);
} else if (childConfig.tagsChildrenWith(_InputDecoratorState._kSuffixSemanticsTag)) {
suffixMergeGroup ??= <SemanticsConfiguration>[];
suffixMergeGroup.add(childConfig);
} else {
builder.markAsMergeUp(childConfig);
}
}
if (prefixMergeGroup != null) {
builder.markAsSiblingMergeGroup(prefixMergeGroup);
}
if (suffixMergeGroup != null) {
builder.markAsSiblingMergeGroup(suffixMergeGroup);
}
return builder.build();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
config.childConfigurationsDelegate = _childSemanticsConfigurationDelegate;
}
@override @override
void performLayout() { void performLayout() {
final BoxConstraints constraints = this.constraints; final BoxConstraints constraints = this.constraints;
_labelTransform = null; _labelTransform = null;
final _RenderDecorationLayout layout = _layout(constraints); final _RenderDecorationLayout layout = _layout(constraints);
size = constraints.constrain(layout.size);
assert(size.width == constraints.constrainWidth(layout.size.width));
assert(size.height == constraints.constrainHeight(layout.size.height));
final double overallWidth = constraints.maxWidth; final double overallWidth = layout.size.width;
final double overallHeight = layout.containerHeight + layout.subtextHeight;
final RenderBox? container = this.container; final RenderBox? container = this.container;
if (container != null) { if (container != null) {
@ -1391,24 +1282,12 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
_boxParentData(container).offset = Offset(x, 0.0); _boxParentData(container).offset = Offset(x, 0.0);
} }
late double height; final double height = layout.containerHeight;
double centerLayout(RenderBox box, double x) { double centerLayout(RenderBox box, double x) {
_boxParentData(box).offset = Offset(x, (height - box.size.height) / 2.0); _boxParentData(box).offset = Offset(x, (height - box.size.height) / 2.0);
return box.size.width; return box.size.width;
} }
late double baseline;
double baselineLayout(RenderBox box, double x) {
_boxParentData(box).offset = Offset(x, baseline - layout.boxToBaseline[box]!);
return box.size.width;
}
final double left = contentPadding.left;
final double right = overallWidth - contentPadding.right;
height = layout.containerHeight;
baseline = _isOutlineAligned ? layout.outlineBaseline : layout.inputBaseline;
if (icon != null) { if (icon != null) {
final double x = switch (textDirection) { final double x = switch (textDirection) {
TextDirection.rtl => overallWidth - icon!.size.width, TextDirection.rtl => overallWidth - icon!.size.width,
@ -1417,12 +1296,39 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
centerLayout(icon!, x); centerLayout(icon!, x);
} }
final double subtextBaseline = (layout.subtextSize?.ascent ?? 0.0) + layout.containerHeight;
final RenderBox? counter = this.counter;
final double helperErrorBaseline = helperError.getDistanceToBaseline(TextBaseline.alphabetic)!;
final double counterBaseline = counter?.getDistanceToBaseline(TextBaseline.alphabetic)! ?? 0.0;
double start, end;
switch (textDirection) {
case TextDirection.ltr:
start = contentPadding.start + _boxSize(icon).width;
end = overallWidth - contentPadding.end;
_boxParentData(helperError).offset = Offset(start, subtextBaseline - helperErrorBaseline);
if (counter != null) {
_boxParentData(counter).offset = Offset(end - counter.size.width, subtextBaseline - counterBaseline);
}
case TextDirection.rtl:
start = overallWidth - contentPadding.start - _boxSize(icon).width;
end = contentPadding.end;
_boxParentData(helperError).offset = Offset(start - helperError.size.width, subtextBaseline - helperErrorBaseline);
if (counter != null) {
_boxParentData(counter).offset = Offset(end, subtextBaseline - counterBaseline);
}
}
final double baseline = layout.baseline;
double baselineLayout(RenderBox box, double x) {
_boxParentData(box).offset = Offset(x, baseline - box.getDistanceToBaseline(TextBaseline.alphabetic)!);
return box.size.width;
}
switch (textDirection) { switch (textDirection) {
case TextDirection.rtl: { case TextDirection.rtl: {
double start = right - _boxSize(icon).width;
double end = left;
if (prefixIcon != null) { if (prefixIcon != null) {
start += contentPadding.right; start += contentPadding.start;
start -= centerLayout(prefixIcon!, start - prefixIcon!.size.width); start -= centerLayout(prefixIcon!, start - prefixIcon!.size.width);
} }
if (label != null) { if (label != null) {
@ -1442,7 +1348,7 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
baselineLayout(hint!, start - hint!.size.width); baselineLayout(hint!, start - hint!.size.width);
} }
if (suffixIcon != null) { if (suffixIcon != null) {
end -= contentPadding.left; end -= contentPadding.end;
end += centerLayout(suffixIcon!, end); end += centerLayout(suffixIcon!, end);
} }
if (suffix != null) { if (suffix != null) {
@ -1451,10 +1357,8 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
break; break;
} }
case TextDirection.ltr: { case TextDirection.ltr: {
double start = left + _boxSize(icon).width;
double end = right;
if (prefixIcon != null) { if (prefixIcon != null) {
start -= contentPadding.left; start -= contentPadding.start;
start += centerLayout(prefixIcon!, start); start += centerLayout(prefixIcon!, start);
} }
if (label != null) { if (label != null) {
@ -1474,7 +1378,7 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
baselineLayout(hint!, start); baselineLayout(hint!, start);
} }
if (suffixIcon != null) { if (suffixIcon != null) {
end += contentPadding.right; end += contentPadding.end;
end -= centerLayout(suffixIcon!, end - suffixIcon!.size.width); end -= centerLayout(suffixIcon!, end - suffixIcon!.size.width);
} }
if (suffix != null) { if (suffix != null) {
@ -1484,28 +1388,6 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
} }
} }
if (helperError != null || counter != null) {
height = layout.subtextHeight;
baseline = layout.subtextBaseline;
switch (textDirection) {
case TextDirection.rtl:
if (helperError != null) {
baselineLayout(helperError!, right - helperError!.size.width - _boxSize(icon).width);
}
if (counter != null) {
baselineLayout(counter!, left);
}
case TextDirection.ltr:
if (helperError != null) {
baselineLayout(helperError!, left + _boxSize(icon).width);
}
if (counter != null) {
baselineLayout(counter!, right - counter!.size.width);
}
}
}
if (label != null) { if (label != null) {
final double labelX = _boxParentData(label!).offset.dx; final double labelX = _boxParentData(label!).offset.dx;
// +1 shifts the range of x from (-1.0, 1.0) to (0.0, 2.0). // +1 shifts the range of x from (-1.0, 1.0) to (0.0, 2.0).
@ -1517,7 +1399,7 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
case TextDirection.rtl: case TextDirection.rtl:
double offsetToPrefixIcon = 0.0; double offsetToPrefixIcon = 0.0;
if (prefixIcon != null && !decoration.alignLabelWithHint) { if (prefixIcon != null && !decoration.alignLabelWithHint) {
offsetToPrefixIcon = material3 ? _boxSize(prefixIcon).width - left : 0; offsetToPrefixIcon = material3 ? _boxSize(prefixIcon).width - contentPadding.end : 0;
} }
decoration.borderGap.start = lerpDouble(labelX + _boxSize(label).width + offsetToPrefixIcon, decoration.borderGap.start = lerpDouble(labelX + _boxSize(label).width + offsetToPrefixIcon,
_boxSize(container).width / 2.0 + floatWidth / 2.0, _boxSize(container).width / 2.0 + floatWidth / 2.0,
@ -1529,7 +1411,7 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
// floating label is centered, it's already relative to _BorderContainer. // floating label is centered, it's already relative to _BorderContainer.
double offsetToPrefixIcon = 0.0; double offsetToPrefixIcon = 0.0;
if (prefixIcon != null && !decoration.alignLabelWithHint) { if (prefixIcon != null && !decoration.alignLabelWithHint) {
offsetToPrefixIcon = material3 ? (-_boxSize(prefixIcon).width + left) : 0; offsetToPrefixIcon = material3 ? (-_boxSize(prefixIcon).width + contentPadding.start) : 0;
} }
decoration.borderGap.start = lerpDouble(labelX - _boxSize(icon).width + offsetToPrefixIcon, decoration.borderGap.start = lerpDouble(labelX - _boxSize(icon).width + offsetToPrefixIcon,
_boxSize(container).width / 2.0 - floatWidth / 2.0, _boxSize(container).width / 2.0 - floatWidth / 2.0,
@ -1540,10 +1422,6 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
decoration.borderGap.start = null; decoration.borderGap.start = null;
decoration.borderGap.extent = 0.0; decoration.borderGap.extent = 0.0;
} }
size = constraints.constrain(Size(overallWidth, overallHeight));
assert(size.width == constraints.constrainWidth(overallWidth));
assert(size.height == constraints.constrainHeight(overallHeight));
} }
void _paintLabel(PaintingContext context, Offset offset) { void _paintLabel(PaintingContext context, Offset offset) {
@ -1584,13 +1462,13 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
startX = labelOffset.dx + labelWidth * (1.0 - scale); startX = labelOffset.dx + labelWidth * (1.0 - scale);
floatStartX = startX; floatStartX = startX;
if (prefixIcon != null && !decoration.alignLabelWithHint && isOutlineBorder) { if (prefixIcon != null && !decoration.alignLabelWithHint && isOutlineBorder) {
floatStartX += material3 ? _boxSize(prefixIcon).width - contentPadding.left : 0.0; floatStartX += material3 ? _boxSize(prefixIcon).width - contentPadding.end : 0.0;
} }
case TextDirection.ltr: // origin on the left case TextDirection.ltr: // origin on the left
startX = labelOffset.dx; startX = labelOffset.dx;
floatStartX = startX; floatStartX = startX;
if (prefixIcon != null && !decoration.alignLabelWithHint && isOutlineBorder) { if (prefixIcon != null && !decoration.alignLabelWithHint && isOutlineBorder) {
floatStartX += material3 ? -_boxSize(prefixIcon).width + contentPadding.left : 0.0; floatStartX += material3 ? -_boxSize(prefixIcon).width + contentPadding.start : 0.0;
} }
} }
final double floatEndX = lerpDouble(floatStartX, centeredFloatX, floatAlign)!; final double floatEndX = lerpDouble(floatStartX, centeredFloatX, floatAlign)!;
@ -1621,6 +1499,17 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
doPaint(counter); doPaint(counter);
} }
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
if (child == label && _labelTransform != null) {
final Offset labelOffset = _boxParentData(label!).offset;
transform
..multiply(_labelTransform!)
..translate(-labelOffset.dx, -labelOffset.dy);
}
super.applyPaintTransform(child, transform);
}
@override @override
bool hitTestSelf(Offset position) => true; bool hitTestSelf(Offset position) => true;
@ -1644,15 +1533,33 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
return false; return false;
} }
@override ChildSemanticsConfigurationsResult _childSemanticsConfigurationDelegate(List<SemanticsConfiguration> childConfigs) {
void applyPaintTransform(RenderObject child, Matrix4 transform) { final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
if (child == label && _labelTransform != null) { List<SemanticsConfiguration>? prefixMergeGroup;
final Offset labelOffset = _boxParentData(label!).offset; List<SemanticsConfiguration>? suffixMergeGroup;
transform for (final SemanticsConfiguration childConfig in childConfigs) {
..multiply(_labelTransform!) if (childConfig.tagsChildrenWith(_InputDecoratorState._kPrefixSemanticsTag)) {
..translate(-labelOffset.dx, -labelOffset.dy); prefixMergeGroup ??= <SemanticsConfiguration>[];
prefixMergeGroup.add(childConfig);
} else if (childConfig.tagsChildrenWith(_InputDecoratorState._kSuffixSemanticsTag)) {
suffixMergeGroup ??= <SemanticsConfiguration>[];
suffixMergeGroup.add(childConfig);
} else {
builder.markAsMergeUp(childConfig);
}
} }
super.applyPaintTransform(child, transform); if (prefixMergeGroup != null) {
builder.markAsSiblingMergeGroup(prefixMergeGroup);
}
if (suffixMergeGroup != null) {
builder.markAsSiblingMergeGroup(suffixMergeGroup);
}
return builder.build();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
config.childConfigurationsDelegate = _childSemanticsConfigurationDelegate;
} }
} }
@ -2416,46 +2323,58 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
// The _Decoration widget and _RenderDecoration assume that contentPadding // The _Decoration widget and _RenderDecoration assume that contentPadding
// has been resolved to EdgeInsets. // has been resolved to EdgeInsets.
final TextDirection textDirection = Directionality.of(context); final TextDirection textDirection = Directionality.of(context);
final EdgeInsets? decorationContentPadding = decoration.contentPadding?.resolve(textDirection); final bool flipHorizontal = switch (textDirection) {
TextDirection.ltr => false,
TextDirection.rtl => true,
};
final EdgeInsets? resolvedPadding = decoration.contentPadding?.resolve(textDirection);
final EdgeInsetsDirectional? decorationContentPadding = resolvedPadding == null
? null
: EdgeInsetsDirectional.fromSTEB(
flipHorizontal ? resolvedPadding.right : resolvedPadding.left,
resolvedPadding.top,
flipHorizontal ? resolvedPadding.left : resolvedPadding.right,
resolvedPadding.bottom,
);
final EdgeInsets contentPadding; final EdgeInsetsDirectional contentPadding;
final double floatingLabelHeight; final double floatingLabelHeight;
if (decoration.isCollapsed ?? themeData.inputDecorationTheme.isCollapsed) { if (decoration.isCollapsed ?? themeData.inputDecorationTheme.isCollapsed) {
floatingLabelHeight = 0.0; floatingLabelHeight = 0.0;
contentPadding = decorationContentPadding ?? EdgeInsets.zero; contentPadding = decorationContentPadding ?? EdgeInsetsDirectional.zero;
} else if (!border.isOutline) { } else if (!border.isOutline) {
// 4.0: the vertical gap between the inline elements and the floating label. // 4.0: the vertical gap between the inline elements and the floating label.
floatingLabelHeight = MediaQuery.textScalerOf(context).scale(4.0 + 0.75 * labelStyle.fontSize!); floatingLabelHeight = MediaQuery.textScalerOf(context).scale(4.0 + 0.75 * labelStyle.fontSize!);
if (decoration.filled ?? false) { if (decoration.filled ?? false) {
contentPadding = decorationContentPadding ?? (Theme.of(context).useMaterial3 contentPadding = decorationContentPadding ?? (Theme.of(context).useMaterial3
? decorationIsDense ? decorationIsDense
? const EdgeInsets.fromLTRB(12.0, 4.0, 12.0, 4.0) ? const EdgeInsetsDirectional.fromSTEB(12.0, 4.0, 12.0, 4.0)
: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0) : const EdgeInsetsDirectional.fromSTEB(12.0, 8.0, 12.0, 8.0)
: decorationIsDense : decorationIsDense
? const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0) ? const EdgeInsetsDirectional.fromSTEB(12.0, 8.0, 12.0, 8.0)
: const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 12.0)); : const EdgeInsetsDirectional.fromSTEB(12.0, 12.0, 12.0, 12.0));
} else { } else {
// No left or right padding for underline borders that aren't filled // No left or right padding for underline borders that aren't filled
// is a small concession to backwards compatibility. This eliminates // is a small concession to backwards compatibility. This eliminates
// the most noticeable layout change introduced by #13734. // the most noticeable layout change introduced by #13734.
contentPadding = decorationContentPadding ?? (Theme.of(context).useMaterial3 contentPadding = decorationContentPadding ?? (Theme.of(context).useMaterial3
? decorationIsDense ? decorationIsDense
? const EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0) ? const EdgeInsetsDirectional.fromSTEB(0.0, 4.0, 0.0, 4.0)
: const EdgeInsets.fromLTRB(0.0, 8.0, 0.0, 8.0) : const EdgeInsetsDirectional.fromSTEB(0.0, 8.0, 0.0, 8.0)
: decorationIsDense : decorationIsDense
? const EdgeInsets.fromLTRB(0.0, 8.0, 0.0, 8.0) ? const EdgeInsetsDirectional.fromSTEB(0.0, 8.0, 0.0, 8.0)
: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 12.0)); : const EdgeInsetsDirectional.fromSTEB(0.0, 12.0, 0.0, 12.0));
} }
} else { } else {
floatingLabelHeight = 0.0; floatingLabelHeight = 0.0;
contentPadding = decorationContentPadding ?? (Theme.of(context).useMaterial3 contentPadding = decorationContentPadding ?? (Theme.of(context).useMaterial3
? decorationIsDense ? decorationIsDense
? const EdgeInsets.fromLTRB(12.0, 16.0, 12.0, 8.0) ? const EdgeInsetsDirectional.fromSTEB(12.0, 16.0, 12.0, 8.0)
: const EdgeInsets.fromLTRB(12.0, 20.0, 12.0, 12.0) : const EdgeInsetsDirectional.fromSTEB(12.0, 20.0, 12.0, 12.0)
: decorationIsDense : decorationIsDense
? const EdgeInsets.fromLTRB(12.0, 20.0, 12.0, 12.0) ? const EdgeInsetsDirectional.fromSTEB(12.0, 20.0, 12.0, 12.0)
: const EdgeInsets.fromLTRB(12.0, 24.0, 12.0, 16.0)); : const EdgeInsetsDirectional.fromSTEB(12.0, 24.0, 12.0, 16.0));
} }
final _Decorator decorator = _Decorator( final _Decorator decorator = _Decorator(

View File

@ -4533,43 +4533,6 @@ void main() {
expect(intrinsicHeight, equals(height)); expect(intrinsicHeight, equals(height));
}); });
testWidgets('Error message for negative baseline', (WidgetTester tester) async {
FlutterErrorDetails? errorDetails;
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
errorDetails ??= details;
};
try {
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: InputDecorator(
decoration: InputDecoration(),
child: Stack(
children: <Widget>[
SizedBox(height: 0),
Positioned(
bottom: 5,
child: Text('ok'),
),
],
),
),
),
),
),
phase: EnginePhase.layout,
);
} finally {
FlutterError.onError = oldHandler;
}
expect(errorDetails?.toString(), contains("InputDecorator's children reported a negative baseline"));
expect(errorDetails?.toString(), contains('RenderStack'));
});
testWidgets('Min intrinsic height for TextField with no content padding', (WidgetTester tester) async { testWidgets('Min intrinsic height for TextField with no content padding', (WidgetTester tester) async {
// Regression test for: https://github.com/flutter/flutter/issues/75509 // Regression test for: https://github.com/flutter/flutter/issues/75509
await tester.pumpWidget(const MaterialApp( await tester.pumpWidget(const MaterialApp(