diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index f43cf04587a23..e1b601d308da0 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -574,7 +574,6 @@ class _RenderDecorationLayout { const _RenderDecorationLayout({ this.boxToBaseline, this.inputBaseline, // for InputBorderType.underline - this.outlineBaseline, // for InputBorderType.outline this.subtextBaseline, this.containerHeight, this.subtextHeight, @@ -582,7 +581,6 @@ class _RenderDecorationLayout { final Map boxToBaseline; final double inputBaseline; - final double outlineBaseline; final double subtextBaseline; // helper/error counter final double containerHeight; final double subtextHeight; @@ -596,6 +594,7 @@ class _RenderDecoration extends RenderBox { @required TextBaseline textBaseline, @required bool isFocused, @required bool expands, + TextAlignVertical textAlignVertical, }) : assert(decoration != null), assert(textDirection != null), assert(textBaseline != null), @@ -603,6 +602,7 @@ class _RenderDecoration extends RenderBox { _decoration = decoration, _textDirection = textDirection, _textBaseline = textBaseline, + _textAlignVertical = textAlignVertical, _isFocused = isFocused, _expands = expands; @@ -746,6 +746,27 @@ class _RenderDecoration extends RenderBox { markNeedsLayout(); } + TextAlignVertical get textAlignVertical { + if (_textAlignVertical == null) { + return _isOutlineAligned ? TextAlignVertical.center : TextAlignVertical.top; + } + return _textAlignVertical; + } + TextAlignVertical _textAlignVertical; + set textAlignVertical(TextAlignVertical value) { + assert(value != null); + if (_textAlignVertical == value) { + return; + } + // No need to relayout if the effective value is still the same. + if (textAlignVertical.y == value.y) { + _textAlignVertical = value; + return; + } + _textAlignVertical = value; + markNeedsLayout(); + } + bool get isFocused => _isFocused; bool _isFocused; set isFocused(bool value) { @@ -766,6 +787,12 @@ class _RenderDecoration extends RenderBox { markNeedsLayout(); } + // Indicates that the decoration should be aligned to accommodate an outline + // border. + bool get _isOutlineAligned { + return !decoration.isCollapsed && decoration.border.isOutline; + } + @override void attach(PipelineOwner owner) { super.attach(owner); @@ -862,7 +889,7 @@ class _RenderDecoration extends RenderBox { EdgeInsets get contentPadding => decoration.contentPadding; - // Lay out the given box if needed, and return its baseline + // Lay out the given box if needed, and return its baseline. double _layoutLineBox(RenderBox box, BoxConstraints constraints) { if (box == null) { return 0.0; @@ -1006,21 +1033,34 @@ class _RenderDecoration extends RenderBox { ? maxContainerHeight : math.min(contentHeight, maxContainerHeight); - // Always position the prefix/suffix in the same place (baseline). + // Try to consider the prefix/suffix as part of the text when aligning it. + // If the prefix/suffix overflows however, allow it to extend outside of the + // input and align the remaining part of the text and prefix/suffix. final double overflow = math.max(0, contentHeight - maxContainerHeight); - final double baselineAdjustment = fixAboveInput - overflow; + // Map textAlignVertical from -1:1 to 0:1 so that it can be used to scale + // the baseline from its minimum to maximum values. + final double textAlignVerticalFactor = (textAlignVertical.y + 1.0) / 2.0; + // Adjust to try to fit top overflow inside the input on an inverse scale of + // textAlignVertical, so that top aligned text adjusts the most and bottom + // aligned text doesn't adjust at all. + final double baselineAdjustment = fixAboveInput - overflow * (1 - textAlignVerticalFactor); // The baselines that will be used to draw the actual input text content. - final double inputBaseline = contentPadding.top + final double topInputBaseline = contentPadding.top + topHeight + inputInternalBaseline + baselineAdjustment; - // The text in the input when an outline border is present is centered - // within the container less 2.0 dps at the top to account for the vertical - // space occupied by the floating label. - final double outlineBaseline = inputInternalBaseline - + baselineAdjustment / 2 - + (containerHeight - (2.0 + inputHeight)) / 2.0; + final double maxContentHeight = containerHeight + - contentPadding.top + - topHeight + - contentPadding.bottom; + final double alignableHeight = fixAboveInput + inputHeight + fixBelowInput; + // When outline aligned, the baseline is vertically centered by default, and + // outlinePadding is used to account for the presence of the border and + // floating label. + final double outlinePadding = _isOutlineAligned ? 10.0 : 0; + final double textAlignVerticalOffset = (maxContentHeight - alignableHeight - outlinePadding) * textAlignVerticalFactor; + final double inputBaseline = topInputBaseline + textAlignVerticalOffset; // Find the positions of the text below the input when it exists. double subtextCounterBaseline = 0; @@ -1050,7 +1090,6 @@ class _RenderDecoration extends RenderBox { boxToBaseline: boxToBaseline, containerHeight: containerHeight, inputBaseline: inputBaseline, - outlineBaseline: outlineBaseline, subtextBaseline: subtextBaseline, subtextHeight: subtextHeight, ); @@ -1160,9 +1199,7 @@ class _RenderDecoration extends RenderBox { final double right = overallWidth - contentPadding.right; height = layout.containerHeight; - baseline = decoration.isCollapsed || !decoration.border.isOutline - ? layout.inputBaseline - : layout.outlineBaseline; + baseline = layout.inputBaseline; if (icon != null) { double x; @@ -1213,12 +1250,13 @@ class _RenderDecoration extends RenderBox { start -= contentPadding.left; start += centerLayout(prefixIcon, start); } - if (label != null) + if (label != null) { if (decoration.alignLabelWithHint) { baselineLayout(label, start); } else { centerLayout(label, start); } + } if (prefix != null) start += baselineLayout(prefix, start); if (input != null) @@ -1512,6 +1550,7 @@ class _RenderDecorationElement extends RenderObjectElement { class _Decorator extends RenderObjectWidget { const _Decorator({ Key key, + @required this.textAlignVertical, @required this.decoration, @required this.textDirection, @required this.textBaseline, @@ -1526,6 +1565,7 @@ class _Decorator extends RenderObjectWidget { final _Decoration decoration; final TextDirection textDirection; final TextBaseline textBaseline; + final TextAlignVertical textAlignVertical; final bool isFocused; final bool expands; @@ -1538,6 +1578,7 @@ class _Decorator extends RenderObjectWidget { decoration: decoration, textDirection: textDirection, textBaseline: textBaseline, + textAlignVertical: textAlignVertical, isFocused: isFocused, expands: expands, ); @@ -1612,6 +1653,7 @@ class InputDecorator extends StatefulWidget { this.decoration, this.baseStyle, this.textAlign, + this.textAlignVertical, this.isFocused = false, this.isHovering = false, this.expands = false, @@ -1643,6 +1685,20 @@ class InputDecorator extends StatefulWidget { /// How the text in the decoration should be aligned horizontally. final TextAlign textAlign; + /// {@template flutter.widgets.inputDecorator.textAlignVertical} + /// How the text should be aligned vertically. + /// + /// Determines the alignment of the baseline within the available space of + /// the input (typically a TextField). For example, TextAlignVertical.top will + /// place the baseline such that the text, and any attached decoration like + /// prefix and suffix, is as close to the top of the input as possible without + /// overflowing. The heights of the prefix and suffix are similarly included + /// for other alignment values. If the height is greater than the height + /// available, then the prefix and suffix will be allowed to overflow first + /// before the text scrolls. + /// {@endtemplate} + final TextAlignVertical textAlignVertical; + /// Whether the input field has focus. /// /// Determines the position of the label text and the color and weight of the @@ -2148,6 +2204,7 @@ class _InputDecoratorState extends State with TickerProviderStat ), textDirection: textDirection, textBaseline: textBaseline, + textAlignVertical: widget.textAlignVertical, isFocused: isFocused, expands: widget.expands, ); @@ -3468,3 +3525,42 @@ class InputDecorationTheme extends Diagnosticable { properties.add(DiagnosticsProperty('alignLabelWithHint', alignLabelWithHint, defaultValue: defaultTheme.alignLabelWithHint)); } } + +/// The vertical alignment of text within an input. +/// +/// A single [y] value that can range from -1.0 to 1.0. -1.0 aligns to the top +/// of the input so that the top of the first line of text fits within the input +/// and its padding. 0.0 aligns to the center of the input. 1.0 aligns so that +/// the bottom of the last line of text aligns with the bottom interior edge of +/// the input. +/// +/// See also: +/// +/// * [TextField.textAlignVertical], which is passed on to the [InputDecorator]. +/// * [InputDecorator.textAlignVertical], which defines the alignment of +/// prefix, input, and suffix, within the [InputDecorator]. +class TextAlignVertical { + /// Construct TextAlignVertical from any given y value. + const TextAlignVertical({ + @required this.y, + }) : assert(y != null), + assert(y >= -1.0 && y <= 1.0); + + /// A value ranging from -1.0 to 1.0 that defines the topmost and bottommost + /// locations of the top and bottom of the input text box. + final double y; + + /// Aligns a TextField's input Text with the topmost location within the + /// TextField. + static const TextAlignVertical top = TextAlignVertical(y: -1.0); + /// Aligns a TextField's input Text to the center of the TextField. + static const TextAlignVertical center = TextAlignVertical(y: 0.0); + /// Aligns a TextField's input Text with the bottommost location within the + /// TextField. + static const TextAlignVertical bottom = TextAlignVertical(y: 1.0); + + @override + String toString() { + return '$runtimeType(y: $y)'; + } +} diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 1c7cc2c3463e8..e8061e2061f55 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -149,6 +149,7 @@ class TextField extends StatefulWidget { this.style, this.strutStyle, this.textAlign = TextAlign.start, + this.textAlignVertical, this.textDirection, this.readOnly = false, this.showCursor, @@ -278,6 +279,9 @@ class TextField extends StatefulWidget { /// {@macro flutter.widgets.editableText.textAlign} final TextAlign textAlign; + /// {@macro flutter.material.inputDecorator.textAlignVertical} + final TextAlignVertical textAlignVertical; + /// {@macro flutter.widgets.editableText.textDirection} final TextDirection textDirection; @@ -506,6 +510,7 @@ class TextField extends StatefulWidget { properties.add(EnumProperty('textInputAction', textInputAction, defaultValue: null)); properties.add(EnumProperty('textCapitalization', textCapitalization, defaultValue: TextCapitalization.none)); properties.add(EnumProperty('textAlign', textAlign, defaultValue: TextAlign.start)); + properties.add(DiagnosticsProperty('textAlignVertical', textAlignVertical, defaultValue: null)); properties.add(EnumProperty('textDirection', textDirection, defaultValue: null)); properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); properties.add(DiagnosticsProperty('cursorRadius', cursorRadius, defaultValue: null)); @@ -1009,6 +1014,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi decoration: _getEffectiveDecoration(), baseStyle: widget.style, textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, isHovering: _isHovering, isFocused: focusNode.hasFocus, isEmpty: controller.value.text.isEmpty, diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index a8b674b7a6536..db37d04703ffb 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -2153,8 +2153,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im renderers.add(renderer); } final Matrix4 transform = Matrix4.identity(); - for (int index = renderers.length - 1; index > 0; index -= 1) + for (int index = renderers.length - 1; index > 0; index -= 1) { renderers[index].applyPaintTransform(renderers[index - 1], transform); + } return transform; } diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 9ad15d8a90565..771262b7df005 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -15,10 +15,12 @@ Widget buildInputDecorator({ InputDecoration decoration = const InputDecoration(), InputDecorationTheme inputDecorationTheme, TextDirection textDirection = TextDirection.ltr, + bool expands = false, bool isEmpty = false, bool isFocused = false, bool isHovering = false, TextStyle baseStyle, + TextAlignVertical textAlignVertical, Widget child = const Text( 'text', style: TextStyle(fontFamily: 'Ahem', fontSize: 16.0), @@ -37,11 +39,13 @@ Widget buildInputDecorator({ child: Directionality( textDirection: textDirection, child: InputDecorator( + expands: expands, decoration: decoration, isEmpty: isEmpty, isFocused: isFocused, isHovering: isHovering, baseStyle: baseStyle, + textAlignVertical: textAlignVertical, child: child, ), ), @@ -277,69 +281,216 @@ void main() { expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy); }); - testWidgets('InputDecorator alignLabelWithHint for multiline TextField no-strut', (WidgetTester tester) async { - Widget buildFrame(bool alignLabelWithHint) { - return MaterialApp( - home: Material( - child: Directionality( - textDirection: TextDirection.ltr, - child: TextField( - maxLines: 8, - decoration: InputDecoration( - labelText: 'label', - alignLabelWithHint: alignLabelWithHint, - hintText: 'hint', + group('alignLabelWithHint', () { + group('expands false', () { + testWidgets('multiline TextField no-strut', (WidgetTester tester) async { + const String text = 'text'; + final FocusNode focusNode = FocusNode(); + final TextEditingController controller = TextEditingController(); + Widget buildFrame(bool alignLabelWithHint) { + return MaterialApp( + home: Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: TextField( + controller: controller, + focusNode: focusNode, + maxLines: 8, + decoration: InputDecoration( + labelText: 'label', + alignLabelWithHint: alignLabelWithHint, + hintText: 'hint', + ), + strutStyle: StrutStyle.disabled, + ), ), - strutStyle: StrutStyle.disabled, ), - ), - ), - ); - } - - // alignLabelWithHint: false centers the label in the TextField - await tester.pumpWidget(buildFrame(false)); - await tester.pumpAndSettle(); - expect(tester.getTopLeft(find.text('label')).dy, 76.0); - expect(tester.getBottomLeft(find.text('label')).dy, 92.0); - - // alignLabelWithHint: true aligns the label with the hint. - await tester.pumpWidget(buildFrame(true)); - await tester.pumpAndSettle(); - expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy); - expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy); - }); - - testWidgets('InputDecorator alignLabelWithHint for multiline TextField', (WidgetTester tester) async { - Widget buildFrame(bool alignLabelWithHint) { - return MaterialApp( - home: Material( - child: Directionality( - textDirection: TextDirection.ltr, - child: TextField( - maxLines: 8, - decoration: InputDecoration( - labelText: 'label', - alignLabelWithHint: alignLabelWithHint, - hintText: 'hint', + ); + } + + // alignLabelWithHint: false centers the label in the TextField. + await tester.pumpWidget(buildFrame(false)); + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('label')).dy, 76.0); + expect(tester.getBottomLeft(find.text('label')).dy, 92.0); + + // Entering text still happens at the top. + await tester.enterText(find.byType(TextField), text); + expect(tester.getTopLeft(find.text(text)).dy, 28.0); + controller.clear(); + focusNode.unfocus(); + + // alignLabelWithHint: true aligns the label with the hint. + await tester.pumpWidget(buildFrame(true)); + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy); + expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy); + + // Entering text still happens at the top. + await tester.enterText(find.byType(TextField), text); + expect(tester.getTopLeft(find.text(text)).dy, 28.0); + controller.clear(); + focusNode.unfocus(); + }); + + testWidgets('multiline TextField', (WidgetTester tester) async { + const String text = 'text'; + final FocusNode focusNode = FocusNode(); + final TextEditingController controller = TextEditingController(); + Widget buildFrame(bool alignLabelWithHint) { + return MaterialApp( + home: Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: TextField( + controller: controller, + focusNode: focusNode, + maxLines: 8, + decoration: InputDecoration( + labelText: 'label', + alignLabelWithHint: alignLabelWithHint, + hintText: 'hint', + ), + ), ), ), - ), - ), - ); - } - - // alignLabelWithHint: false centers the label in the TextField - await tester.pumpWidget(buildFrame(false)); - await tester.pumpAndSettle(); - expect(tester.getTopLeft(find.text('label')).dy, 76.0); - expect(tester.getBottomLeft(find.text('label')).dy, 92.0); - - // alignLabelWithHint: true aligns the label with the hint. - await tester.pumpWidget(buildFrame(true)); - await tester.pumpAndSettle(); - expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy); - expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy); + ); + } + + // alignLabelWithHint: false centers the label in the TextField. + await tester.pumpWidget(buildFrame(false)); + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('label')).dy, 76.0); + expect(tester.getBottomLeft(find.text('label')).dy, 92.0); + + // Entering text still happens at the top. + await tester.enterText(find.byType(InputDecorator), text); + expect(tester.getTopLeft(find.text(text)).dy, 28.0); + controller.clear(); + focusNode.unfocus(); + + // alignLabelWithHint: true aligns the label with the hint. + await tester.pumpWidget(buildFrame(true)); + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy); + expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy); + + // Entering text still happens at the top. + await tester.enterText(find.byType(InputDecorator), text); + expect(tester.getTopLeft(find.text(text)).dy, 28.0); + controller.clear(); + focusNode.unfocus(); + }); + }); + + group('expands true', () { + testWidgets('multiline TextField', (WidgetTester tester) async { + const String text = 'text'; + final FocusNode focusNode = FocusNode(); + final TextEditingController controller = TextEditingController(); + Widget buildFrame(bool alignLabelWithHint) { + return MaterialApp( + home: Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: TextField( + controller: controller, + focusNode: focusNode, + maxLines: null, + expands: true, + decoration: InputDecoration( + labelText: 'label', + alignLabelWithHint: alignLabelWithHint, + hintText: 'hint', + ), + ), + ), + ), + ); + } + + // alignLabelWithHint: false centers the label in the TextField. + await tester.pumpWidget(buildFrame(false)); + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('label')).dy, 292.0); + expect(tester.getBottomLeft(find.text('label')).dy, 308.0); + + // Entering text still happens at the top. + await tester.enterText(find.byType(InputDecorator), text); + expect(tester.getTopLeft(find.text(text)).dy, 28.0); + controller.clear(); + focusNode.unfocus(); + + // alignLabelWithHint: true aligns the label with the hint at the top. + await tester.pumpWidget(buildFrame(true)); + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('label')).dy, 28.0); + expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy); + expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy); + + // Entering text still happens at the top. + await tester.enterText(find.byType(InputDecorator), text); + expect(tester.getTopLeft(find.text(text)).dy, 28.0); + controller.clear(); + focusNode.unfocus(); + }); + + testWidgets('multiline TextField with outline border', (WidgetTester tester) async { + const String text = 'text'; + final FocusNode focusNode = FocusNode(); + final TextEditingController controller = TextEditingController(); + Widget buildFrame(bool alignLabelWithHint) { + return MaterialApp( + home: Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: TextField( + controller: controller, + focusNode: focusNode, + maxLines: null, + expands: true, + decoration: InputDecoration( + labelText: 'label', + alignLabelWithHint: alignLabelWithHint, + hintText: 'hint', + border: OutlineInputBorder( + borderSide: const BorderSide(width: 1, color: Colors.black, style: BorderStyle.solid), + borderRadius: BorderRadius.circular(0), + ), + ), + ), + ), + ), + ); + } + + // alignLabelWithHint: false centers the label in the TextField. + await tester.pumpWidget(buildFrame(false)); + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('label')).dy, 292.0); + expect(tester.getBottomLeft(find.text('label')).dy, 308.0); + + // Entering text happens in the center as well. + await tester.enterText(find.byType(InputDecorator), text); + expect(tester.getTopLeft(find.text(text)).dy, 291.0); + controller.clear(); + focusNode.unfocus(); + + // alignLabelWithHint: true aligns keeps the label in the center because + // that's where the hint is. + await tester.pumpWidget(buildFrame(true)); + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('label')).dy, 291.0); + expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy); + expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy); + + // Entering text still happens in the center. + await tester.enterText(find.byType(InputDecorator), text); + expect(tester.getTopLeft(find.text(text)).dy, 291.0); + controller.clear(); + focusNode.unfocus(); + }); + }); }); // Overall height for this InputDecorator is 40.0dps @@ -1178,6 +1329,471 @@ void main() { expect(tester.getTopLeft(find.byKey(prefixKey)).dy, 0.0); }); + group('textAlignVertical position', () { + group('simple case', () { + testWidgets('align top (default)', (WidgetTester tester) async { + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, // so we have a tall input where align can vary + decoration: const InputDecoration( + filled: true, + ), + textAlignVertical: TextAlignVertical.top, // default when no border + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // Same as the default case above. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(12.0, .0001)); + }); + + testWidgets('align center', (WidgetTester tester) async { + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, + decoration: const InputDecoration( + filled: true, + ), + textAlignVertical: TextAlignVertical.center, + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // Below the top aligned case. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(290.0, .0001)); + }); + + testWidgets('align bottom', (WidgetTester tester) async { + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, + decoration: const InputDecoration( + filled: true, + ), + textAlignVertical: TextAlignVertical.bottom, + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // Below the center aligned case. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(568.0, .0001)); + }); + + testWidgets('align as a double', (WidgetTester tester) async { + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, + decoration: const InputDecoration( + filled: true, + ), + textAlignVertical: const TextAlignVertical(y: 0.75), + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // In between the center and bottom aligned cases. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(498.5, .0001)); + }); + }); + + group('outline border', () { + testWidgets('align top', (WidgetTester tester) async { + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, // so we have a tall input where align can vary + decoration: const InputDecoration( + filled: true, + border: OutlineInputBorder(), + ), + textAlignVertical: TextAlignVertical.top, + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // Similar to the case without a border, but with a little extra room at + // the top to make room for the border. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(24.0, .0001)); + }); + + testWidgets('align center (default)', (WidgetTester tester) async { + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, + decoration: const InputDecoration( + filled: true, + border: OutlineInputBorder(), + ), + textAlignVertical: TextAlignVertical.center, // default when border + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // Below the top aligned case. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(289.0, .0001)); + }); + + testWidgets('align bottom', (WidgetTester tester) async { + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, + decoration: const InputDecoration( + filled: true, + border: OutlineInputBorder(), + ), + textAlignVertical: TextAlignVertical.bottom, + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // Below the center aligned case. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(554.0, .0001)); + }); + }); + + group('prefix', () { + testWidgets('InputDecorator tall prefix align top', (WidgetTester tester) async { + const Key pKey = Key('p'); + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + decoration: InputDecoration( + prefix: Container( + key: pKey, + height: 100, + width: 10, + ), + filled: true, + ), + textAlignVertical: TextAlignVertical.top, // default when no border + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // Same as the default case above. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(96, .0001)); + expect(tester.getTopLeft(find.byKey(pKey)).dy, 12.0); + }); + + testWidgets('InputDecorator tall prefix align center', (WidgetTester tester) async { + const Key pKey = Key('p'); + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + decoration: InputDecoration( + prefix: Container( + key: pKey, + height: 100, + width: 10, + ), + filled: true, + ), + textAlignVertical: TextAlignVertical.center, + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // Same as the default case above. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(96.0, .0001)); + expect(tester.getTopLeft(find.byKey(pKey)).dy, 12.0); + }); + + testWidgets('InputDecorator tall prefix align bottom', (WidgetTester tester) async { + const Key pKey = Key('p'); + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + decoration: InputDecoration( + prefix: Container( + key: pKey, + height: 100, + width: 10, + ), + filled: true, + ), + textAlignVertical: TextAlignVertical.bottom, + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // Top of the input + 100 prefix height - overlap + expect(tester.getTopLeft(find.text(text)).dy, closeTo(96.0, .0001)); + expect(tester.getTopLeft(find.byKey(pKey)).dy, 12.0); + }); + }); + + group('outline border and prefix', () { + testWidgets('InputDecorator tall prefix align center', (WidgetTester tester) async { + const Key pKey = Key('p'); + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + prefix: Container( + key: pKey, + height: 100, + width: 10, + ), + filled: true, + ), + textAlignVertical: TextAlignVertical.center, // default when border + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // In the middle of the expanded InputDecorator. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(331.0, .0001)); + expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(247.0, .0001)); + }); + + testWidgets('InputDecorator tall prefix with border align top', (WidgetTester tester) async { + const Key pKey = Key('p'); + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + prefix: Container( + key: pKey, + height: 100, + width: 10, + ), + filled: true, + ), + textAlignVertical: TextAlignVertical.top, + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // Above the center example. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(108.0, .0001)); + // The prefix is positioned at the top of the input, so this value is + // the same as the top aligned test without a prefix. + expect(tester.getTopLeft(find.byKey(pKey)).dy, 24.0); + }); + + testWidgets('InputDecorator tall prefix with border align bottom', (WidgetTester tester) async { + const Key pKey = Key('p'); + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + prefix: Container( + key: pKey, + height: 100, + width: 10, + ), + filled: true, + ), + textAlignVertical: TextAlignVertical.bottom, + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // Below the center example. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(554.0, .0001)); + expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(470.0, .0001)); + }); + + testWidgets('InputDecorator tall prefix with border align double', (WidgetTester tester) async { + const Key pKey = Key('p'); + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + prefix: Container( + key: pKey, + height: 100, + width: 10, + ), + filled: true, + ), + textAlignVertical: const TextAlignVertical(y: 0.1), + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // Between the top and center examples. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(353.3, .0001)); + expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(269.3, .0001)); + }); + }); + + group('label', () { + testWidgets('align top (default)', (WidgetTester tester) async { + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, // so we have a tall input where align can vary + decoration: const InputDecoration( + labelText: 'label', + filled: true, + ), + textAlignVertical: TextAlignVertical.top, // default + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // The label causes the text to start slightly lower than it would + // otherwise. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(28.0, .0001)); + }); + + testWidgets('align center', (WidgetTester tester) async { + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, // so we have a tall input where align can vary + decoration: const InputDecoration( + labelText: 'label', + filled: true, + ), + textAlignVertical: TextAlignVertical.center, + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // The label reduces the amount of space available for text, so the + // center is slightly lower. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(298.0, .0001)); + }); + + testWidgets('align bottom', (WidgetTester tester) async { + const String text = 'text'; + await tester.pumpWidget( + buildInputDecorator( + // isEmpty: false (default) + // isFocused: false (default) + expands: true, // so we have a tall input where align can vary + decoration: const InputDecoration( + labelText: 'label', + filled: true, + ), + textAlignVertical: TextAlignVertical.bottom, + // Set the fontSize so that everything works out to whole numbers. + child: const Text( + text, + style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0), + ), + ), + ); + + // The label reduces the amount of space available for text, but the + // bottom line is still in the same place. + expect(tester.getTopLeft(find.text(text)).dy, closeTo(568.0, .0001)); + }); + }); + }); + testWidgets('counter text has correct right margin - LTR, not dense', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator(