Fix Vertical Alignment Regression (#34859)

Change the way outlined inputs vertically align their text to be more similar to how it used to be before a refactor. Fixes an edge case uncovered by a SCUBA test.
diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart
index e1b601d..4737888 100644
--- a/packages/flutter/lib/src/material/input_decorator.dart
+++ b/packages/flutter/lib/src/material/input_decorator.dart
@@ -574,6 +574,7 @@
   const _RenderDecorationLayout({
     this.boxToBaseline,
     this.inputBaseline, // for InputBorderType.underline
+    this.outlineBaseline, // for InputBorderType.outline
     this.subtextBaseline,
     this.containerHeight,
     this.subtextHeight,
@@ -581,6 +582,7 @@
 
   final Map<RenderBox, double> boxToBaseline;
   final double inputBaseline;
+  final double outlineBaseline;
   final double subtextBaseline; // helper/error counter
   final double containerHeight;
   final double subtextHeight;
@@ -1055,13 +1057,32 @@
       - 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 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
+    //
+    //  * top (-1.0): topmost point considering padding.
+    //  * center (0.0): the absolute center of the input ignoring padding but
+    //      accommodating the border and floating label.
+    //  * bottom (1.0): bottommost point considering padding.
+    //
+    // That means that if the padding is uneven, center is not the exact
+    // midpoint of top and bottom. To account for this, the above center and
+    // below center alignments are interpolated independently.
+    final double outlineCenterBaseline = inputInternalBaseline
+      + baselineAdjustment / 2.0
+      + (containerHeight - (2.0 + inputHeight)) / 2.0;
+    final double outlineTopBaseline = topInputBaseline;
+    final double outlineBottomBaseline = topInputBaseline + maxVerticalOffset;
+    final double outlineBaseline = _interpolateThree(
+      outlineTopBaseline,
+      outlineCenterBaseline,
+      outlineBottomBaseline,
+      textAlignVertical,
+    );
+
     // Find the positions of the text below the input when it exists.
     double subtextCounterBaseline = 0;
     double subtextHelperBaseline = 0;
@@ -1090,11 +1111,41 @@
       boxToBaseline: boxToBaseline,
       containerHeight: containerHeight,
       inputBaseline: inputBaseline,
+      outlineBaseline: outlineBaseline,
       subtextBaseline: subtextBaseline,
       subtextHeight: subtextHeight,
     );
   }
 
+  // Interpolate between three stops using textAlignVertical. This is used to
+  // calculate the outline baseline, which ignores padding when the alignment is
+  // middle. When the alignment is less than zero, it interpolates between the
+  // centered text box's top and the top of the content padding. When the
+  // 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
+  // padding.
+  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
+      // excessive padding. Those cases are handled by using middle.
+      if (begin >= middle) {
+        return middle;
+      }
+      // 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
   double computeMinIntrinsicWidth(double height) {
     return _minWidth(icon, height)
@@ -1199,7 +1250,7 @@
     final double right = overallWidth - contentPadding.right;
 
     height = layout.containerHeight;
-    baseline = layout.inputBaseline;
+    baseline = _isOutlineAligned ? layout.outlineBaseline : layout.inputBaseline;
 
     if (icon != null) {
       double x;
diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart
index a34039d..86ca4cc 100644
--- a/packages/flutter/test/material/input_decorator_test.dart
+++ b/packages/flutter/test/material/input_decorator_test.dart
@@ -1495,7 +1495,7 @@
         );
 
         // Below the center aligned case.
-        expect(tester.getTopLeft(find.text(text)).dy, closeTo(554.0, .0001));
+        expect(tester.getTopLeft(find.text(text)).dy, closeTo(564.0, .0001));
       });
     });
 
@@ -1680,8 +1680,8 @@
         );
 
         // 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));
+        expect(tester.getTopLeft(find.text(text)).dy, closeTo(564.0, .0001));
+        expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(480.0, .0001));
       });
 
       testWidgets('InputDecorator tall prefix with border align double', (WidgetTester tester) async {
@@ -1711,8 +1711,8 @@
         );
 
         // 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));
+        expect(tester.getTopLeft(find.text(text)).dy, closeTo(354.3, .0001));
+        expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(270.3, .0001));
       });
     });
 
@@ -1794,6 +1794,199 @@
     });
   });
 
+  group('OutlineInputBorder', () {
+    group('default alignment', () {
+      testWidgets('Centers when border', (WidgetTester tester) async {
+        await tester.pumpWidget(
+          buildInputDecorator(
+            decoration: const InputDecoration(
+              border: OutlineInputBorder(),
+            ),
+          ),
+        );
+
+        expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
+        expect(tester.getTopLeft(find.text('text')).dy, 19.0);
+        expect(tester.getBottomLeft(find.text('text')).dy, 35.0);
+        expect(getBorderBottom(tester), 56.0);
+        expect(getBorderWeight(tester), 1.0);
+      });
+
+      testWidgets('Centers when border and label', (WidgetTester tester) async {
+        await tester.pumpWidget(
+          buildInputDecorator(
+            decoration: const InputDecoration(
+              labelText: 'label',
+              border: OutlineInputBorder(),
+            ),
+          ),
+        );
+
+        expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
+        expect(tester.getTopLeft(find.text('text')).dy, 19.0);
+        expect(tester.getBottomLeft(find.text('text')).dy, 35.0);
+        expect(getBorderBottom(tester), 56.0);
+        expect(getBorderWeight(tester), 1.0);
+      });
+
+      testWidgets('Centers when border and contentPadding', (WidgetTester tester) async {
+        await tester.pumpWidget(
+          buildInputDecorator(
+            decoration: const InputDecoration(
+              border: OutlineInputBorder(),
+              contentPadding: EdgeInsets.fromLTRB(
+                12.0, 14.0,
+                8.0, 14.0,
+              ),
+            ),
+          ),
+        );
+
+        expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 44.0));
+        expect(tester.getTopLeft(find.text('text')).dy, 13.0);
+        expect(tester.getBottomLeft(find.text('text')).dy, 29.0);
+        expect(getBorderBottom(tester), 44.0);
+        expect(getBorderWeight(tester), 1.0);
+      });
+
+      testWidgets('Centers when border and contentPadding and label', (WidgetTester tester) async {
+        await tester.pumpWidget(
+          buildInputDecorator(
+            decoration: const InputDecoration(
+              labelText: 'label',
+              border: OutlineInputBorder(),
+              contentPadding: EdgeInsets.fromLTRB(
+                12.0, 14.0,
+                8.0, 14.0,
+              ),
+            ),
+          ),
+        );
+
+        expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 44.0));
+        expect(tester.getTopLeft(find.text('text')).dy, 13.0);
+        expect(tester.getBottomLeft(find.text('text')).dy, 29.0);
+        expect(getBorderBottom(tester), 44.0);
+        expect(getBorderWeight(tester), 1.0);
+      });
+
+      testWidgets('Centers when border and lopsided contentPadding and label', (WidgetTester tester) async {
+        await tester.pumpWidget(
+          buildInputDecorator(
+            decoration: const InputDecoration(
+              labelText: 'label',
+              border: OutlineInputBorder(),
+              contentPadding: EdgeInsets.fromLTRB(
+                12.0, 104.0,
+                8.0, 0.0,
+              ),
+            ),
+          ),
+        );
+
+        expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 120.0));
+        expect(tester.getTopLeft(find.text('text')).dy, 51.0);
+        expect(tester.getBottomLeft(find.text('text')).dy, 67.0);
+        expect(getBorderBottom(tester), 120.0);
+        expect(getBorderWeight(tester), 1.0);
+      });
+    });
+
+    group('3 point interpolation alignment', () {
+      testWidgets('top align includes padding', (WidgetTester tester) async {
+        await tester.pumpWidget(
+          buildInputDecorator(
+            expands: true,
+            textAlignVertical: TextAlignVertical.top,
+            decoration: const InputDecoration(
+              border: OutlineInputBorder(),
+              contentPadding: EdgeInsets.fromLTRB(
+                12.0, 24.0,
+                8.0, 2.0,
+              ),
+            ),
+          ),
+        );
+
+        expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 600.0));
+        // Aligned to the top including the 24px padding.
+        expect(tester.getTopLeft(find.text('text')).dy, 24.0);
+        expect(tester.getBottomLeft(find.text('text')).dy, 40.0);
+        expect(getBorderBottom(tester), 600.0);
+        expect(getBorderWeight(tester), 1.0);
+      });
+
+      testWidgets('center align ignores padding', (WidgetTester tester) async {
+        await tester.pumpWidget(
+          buildInputDecorator(
+            expands: true,
+            textAlignVertical: TextAlignVertical.center,
+            decoration: const InputDecoration(
+              border: OutlineInputBorder(),
+              contentPadding: EdgeInsets.fromLTRB(
+                12.0, 24.0,
+                8.0, 2.0,
+              ),
+            ),
+          ),
+        );
+
+        expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 600.0));
+        // Baseline is on the center of the 600px high input.
+        expect(tester.getTopLeft(find.text('text')).dy, 291.0);
+        expect(tester.getBottomLeft(find.text('text')).dy, 307.0);
+        expect(getBorderBottom(tester), 600.0);
+        expect(getBorderWeight(tester), 1.0);
+      });
+
+      testWidgets('bottom align includes padding', (WidgetTester tester) async {
+        await tester.pumpWidget(
+          buildInputDecorator(
+            expands: true,
+            textAlignVertical: TextAlignVertical.bottom,
+            decoration: const InputDecoration(
+              border: OutlineInputBorder(),
+              contentPadding: EdgeInsets.fromLTRB(
+                12.0, 24.0,
+                8.0, 2.0,
+              ),
+            ),
+          ),
+        );
+
+        expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 600.0));
+        // Includes bottom padding of 2px.
+        expect(tester.getTopLeft(find.text('text')).dy, 582.0);
+        expect(tester.getBottomLeft(find.text('text')).dy, 598.0);
+        expect(getBorderBottom(tester), 600.0);
+        expect(getBorderWeight(tester), 1.0);
+      });
+
+      testWidgets('padding exceeds middle keeps top at middle', (WidgetTester tester) async {
+        await tester.pumpWidget(
+          buildInputDecorator(
+            expands: true,
+            textAlignVertical: TextAlignVertical.top,
+            decoration: const InputDecoration(
+              border: OutlineInputBorder(),
+              contentPadding: EdgeInsets.fromLTRB(
+                12.0, 504.0,
+                8.0, 0.0,
+              ),
+            ),
+          ),
+        );
+
+        expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 600.0));
+        // Same position as the center example above.
+        expect(tester.getTopLeft(find.text('text')).dy, 291.0);
+        expect(tester.getBottomLeft(find.text('text')).dy, 307.0);
+        expect(getBorderBottom(tester), 600.0);
+        expect(getBorderWeight(tester), 1.0);
+      });
+    });
+  });
+
   testWidgets('counter text has correct right margin - LTR, not dense', (WidgetTester tester) async {
     await tester.pumpWidget(
       buildInputDecorator(