Added AxisOrientation property to Scrollbar (#75497)

diff --git a/packages/flutter/lib/src/cupertino/scrollbar.dart b/packages/flutter/lib/src/cupertino/scrollbar.dart
index f01eb7f..f49551d 100644
--- a/packages/flutter/lib/src/cupertino/scrollbar.dart
+++ b/packages/flutter/lib/src/cupertino/scrollbar.dart
@@ -62,6 +62,7 @@
     Radius radius = defaultRadius,
     this.radiusWhileDragging = defaultRadiusWhileDragging,
     ScrollNotificationPredicate? notificationPredicate,
+    ScrollbarOrientation? scrollbarOrientation,
   }) : assert(thickness != null),
        assert(thickness < double.infinity),
        assert(thicknessWhileDragging != null),
@@ -79,6 +80,7 @@
          timeToFade: _kScrollbarTimeToFade,
          pressDuration: const Duration(milliseconds: 100),
          notificationPredicate: notificationPredicate ?? defaultScrollNotificationPredicate,
+         scrollbarOrientation: scrollbarOrientation,
        );
 
   /// Default value for [thickness] if it's not specified in [CupertinoScrollbar].
@@ -148,7 +150,8 @@
       ..radius = _radius
       ..padding = MediaQuery.of(context).padding
       ..minLength = _kScrollbarMinLength
-      ..minOverscrollLength = _kScrollbarMinOverscrollLength;
+      ..minOverscrollLength = _kScrollbarMinOverscrollLength
+      ..scrollbarOrientation = widget.scrollbarOrientation;
   }
 
   double _pressStartAxisPosition = 0.0;
diff --git a/packages/flutter/lib/src/material/scrollbar.dart b/packages/flutter/lib/src/material/scrollbar.dart
index cd445c4..3f53f6b 100644
--- a/packages/flutter/lib/src/material/scrollbar.dart
+++ b/packages/flutter/lib/src/material/scrollbar.dart
@@ -117,6 +117,7 @@
     this.radius,
     this.notificationPredicate,
     this.interactive,
+    this.scrollbarOrientation,
   }) : super(key: key);
 
   /// {@macro flutter.widgets.Scrollbar.child}
@@ -165,6 +166,9 @@
   /// {@macro flutter.widgets.Scrollbar.notificationPredicate}
   final ScrollNotificationPredicate? notificationPredicate;
 
+  /// {@macro flutter.widgets.Scrollbar.scrollbarOrientation}
+  final ScrollbarOrientation? scrollbarOrientation;
+
   @override
   State<Scrollbar> createState() => _ScrollbarState();
 }
@@ -183,6 +187,7 @@
         radiusWhileDragging: widget.radius ?? CupertinoScrollbar.defaultRadiusWhileDragging,
         controller: widget.controller,
         notificationPredicate: widget.notificationPredicate,
+        scrollbarOrientation: widget.scrollbarOrientation,
         child: widget.child,
       );
     }
@@ -195,6 +200,7 @@
       radius: widget.radius,
       notificationPredicate: widget.notificationPredicate,
       interactive: widget.interactive,
+      scrollbarOrientation: widget.scrollbarOrientation,
       child: widget.child,
     );
   }
@@ -212,6 +218,7 @@
     Radius? radius,
     ScrollNotificationPredicate? notificationPredicate,
     bool? interactive,
+    ScrollbarOrientation? scrollbarOrientation,
   }) : super(
          key: key,
          child: child,
@@ -224,6 +231,7 @@
          pressDuration: Duration.zero,
          notificationPredicate: notificationPredicate ?? defaultScrollNotificationPredicate,
          interactive: interactive,
+         scrollbarOrientation: scrollbarOrientation,
        );
 
   final bool? showTrackOnHover;
@@ -380,7 +388,8 @@
       ..crossAxisMargin = _scrollbarTheme.crossAxisMargin ?? (_useAndroidScrollbar ? 0.0 : _kScrollbarMargin)
       ..mainAxisMargin = _scrollbarTheme.mainAxisMargin ?? 0.0
       ..minLength = _scrollbarTheme.minThumbLength ?? _kScrollbarMinLength
-      ..padding = MediaQuery.of(context).padding;
+      ..padding = MediaQuery.of(context).padding
+      ..scrollbarOrientation = widget.scrollbarOrientation;
   }
 
   @override
diff --git a/packages/flutter/lib/src/widgets/scrollbar.dart b/packages/flutter/lib/src/widgets/scrollbar.dart
index 248a1dc..7c451cc 100644
--- a/packages/flutter/lib/src/widgets/scrollbar.dart
+++ b/packages/flutter/lib/src/widgets/scrollbar.dart
@@ -29,6 +29,21 @@
 const Duration _kScrollbarFadeDuration = Duration(milliseconds: 300);
 const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600);
 
+/// An orientation along either the horizontal or vertical [Axis].
+enum ScrollbarOrientation {
+  /// Place towards the left of the screen.
+  left,
+
+  /// Place towards the right of the screen.
+  right,
+
+  /// Place on top of the screen.
+  top,
+
+  /// Place on the bottom of the screen.
+  bottom,
+}
+
 /// Paints a scrollbar's track and thumb.
 ///
 /// The size of the scrollbar along its scroll direction is typically
@@ -72,6 +87,7 @@
     Radius? radius,
     double minLength = _kMinThumbExtent,
     double? minOverscrollLength,
+    ScrollbarOrientation? scrollbarOrientation,
   }) : assert(color != null),
        assert(thickness != null),
        assert(fadeoutOpacityAnimation != null),
@@ -93,6 +109,7 @@
        _minLength = minLength,
        _trackColor = trackColor,
        _trackBorderColor = trackBorderColor,
+       _scrollbarOrientation = scrollbarOrientation,
        _minOverscrollLength = minOverscrollLength ?? minLength {
     fadeoutOpacityAnimation.addListener(notifyListeners);
   }
@@ -270,6 +287,46 @@
     notifyListeners();
   }
 
+  /// {@template flutter.widgets.Scrollbar.scrollbarOrientation}
+  /// Dictates the orientation of the scrollbar.
+  ///
+  /// [ScrollbarOrientation.top] places the scrollbar on top of the screen.
+  /// [ScrollbarOrientation.bottom] places the scrollbar on the bottom of the screen.
+  /// [ScrollbarOrientation.left] places the scrollbar on the left of the screen.
+  /// [ScrollbarOrientation.right] places the scrollbar on the right of the screen.
+  ///
+  /// [ScrollbarOrientation.top] and [ScrollbarOrientation.bottom] can only be
+  /// used with a vertical scroll.
+  /// [ScrollbarOrientation.left] and [ScrollbarOrientation.right] can only be
+  /// used with a horizontal scroll.
+  ///
+  /// For a vertical scroll the orientation defaults to
+  /// [ScrollbarOrientation.right] for [TextDirection.ltr] and
+  /// [ScrollbarOrientation.left] for [TextDirection.rtl].
+  /// For a horizontal scroll the orientation defaults to [ScrollbarOrientation.bottom].
+  /// {@endtemplate}
+  ScrollbarOrientation? get scrollbarOrientation => _scrollbarOrientation;
+  ScrollbarOrientation? _scrollbarOrientation;
+  set scrollbarOrientation(ScrollbarOrientation? value) {
+    if (scrollbarOrientation == value)
+      return;
+
+    _scrollbarOrientation = value;
+    notifyListeners();
+  }
+
+  void _debugAssertIsValidOrientation(ScrollbarOrientation orientation) {
+    assert(
+    (_isVertical && _isVerticalOrientation(orientation)) || (!_isVertical && !_isVerticalOrientation(orientation)),
+    'The given ScrollbarOrientation: $orientation is incompatible with the current AxisDirection: $_lastAxisDirection.'
+    );
+  }
+
+  /// Check whether given scrollbar orientation is vertical
+  bool _isVerticalOrientation(ScrollbarOrientation orientation) =>
+    orientation == ScrollbarOrientation.left
+    || orientation == ScrollbarOrientation.right;
+
   ScrollMetrics? _lastMetrics;
   AxisDirection? _lastAxisDirection;
   Rect? _thumbRect;
@@ -317,37 +374,48 @@
       'A TextDirection must be provided before a Scrollbar can be painted.',
     );
 
+    final ScrollbarOrientation resolvedOrientation;
+
+    if (scrollbarOrientation == null) {
+      if (_isVertical)
+        resolvedOrientation = textDirection == TextDirection.ltr
+          ? ScrollbarOrientation.right
+          : ScrollbarOrientation.left;
+      else
+        resolvedOrientation = ScrollbarOrientation.bottom;
+    }
+    else {
+      resolvedOrientation = scrollbarOrientation!;
+    }
+
     final double x, y;
     final Size thumbSize, trackSize;
     final Offset trackOffset;
 
-    switch (direction) {
-      case AxisDirection.down:
+    _debugAssertIsValidOrientation(resolvedOrientation);
+    switch(resolvedOrientation) {
+      case ScrollbarOrientation.left:
         thumbSize = Size(thickness, thumbExtent);
         trackSize = Size(thickness + 2 * crossAxisMargin, _trackExtent);
-        x = textDirection == TextDirection.rtl
-          ? crossAxisMargin + padding.left
-          : size.width - thickness - crossAxisMargin - padding.right;
+        x = crossAxisMargin + padding.left;
         y = _thumbOffset;
         trackOffset = Offset(x - crossAxisMargin, 0.0);
         break;
-      case AxisDirection.up:
+      case ScrollbarOrientation.right:
         thumbSize = Size(thickness, thumbExtent);
         trackSize = Size(thickness + 2 * crossAxisMargin, _trackExtent);
-        x = textDirection == TextDirection.rtl
-          ? crossAxisMargin + padding.left
-          : size.width - thickness - crossAxisMargin - padding.right;
+        x = size.width - thickness - crossAxisMargin - padding.right;
         y = _thumbOffset;
         trackOffset = Offset(x - crossAxisMargin, 0.0);
         break;
-      case AxisDirection.left:
+      case ScrollbarOrientation.top:
         thumbSize = Size(thumbExtent, thickness);
-        x = _thumbOffset;
-        y = size.height - thickness - crossAxisMargin - padding.bottom;
         trackSize = Size(_trackExtent, thickness + 2 * crossAxisMargin);
+        x = _thumbOffset;
+        y = crossAxisMargin + padding.top;
         trackOffset = Offset(0.0, y - crossAxisMargin);
         break;
-      case AxisDirection.right:
+      case ScrollbarOrientation.bottom:
         thumbSize = Size(thumbExtent, thickness);
         trackSize = Size(_trackExtent, thickness + 2 * crossAxisMargin);
         x = _thumbOffset;
@@ -570,7 +638,8 @@
         || radius != old.radius
         || minLength != old.minLength
         || padding != old.padding
-        || minOverscrollLength != old.minOverscrollLength;
+        || minOverscrollLength != old.minOverscrollLength
+        || scrollbarOrientation != old.scrollbarOrientation;
   }
 
   @override
@@ -655,6 +724,7 @@
     this.pressDuration = Duration.zero,
     this.notificationPredicate = defaultScrollNotificationPredicate,
     this.interactive,
+    this.scrollbarOrientation,
   }) : assert(child != null),
        assert(fadeDuration != null),
        assert(timeToFade != null),
@@ -866,6 +936,9 @@
   /// {@endtemplate}
   final bool? interactive;
 
+  /// {@macro flutter.widgets.Scrollbar.scrollbarOrientation}
+  final ScrollbarOrientation? scrollbarOrientation;
+
   @override
   RawScrollbarState<RawScrollbar> createState() => RawScrollbarState<RawScrollbar>();
 }
@@ -937,6 +1010,7 @@
       color: widget.thumbColor ?? const Color(0x66BCBCBC),
       thickness: widget.thickness ?? _kScrollbarThickness,
       fadeoutOpacityAnimation: _fadeoutOpacityAnimation,
+      scrollbarOrientation: widget.scrollbarOrientation,
     );
   }
 
@@ -1042,7 +1116,8 @@
       ..textDirection = Directionality.of(context)
       ..thickness = widget.thickness ?? _kScrollbarThickness
       ..radius = widget.radius
-      ..padding = MediaQuery.of(context).padding;
+      ..padding = MediaQuery.of(context).padding
+      ..scrollbarOrientation = widget.scrollbarOrientation;
   }
 
   @override
diff --git a/packages/flutter/test/cupertino/scrollbar_test.dart b/packages/flutter/test/cupertino/scrollbar_test.dart
index 8977e2a..1240e48 100644
--- a/packages/flutter/test/cupertino/scrollbar_test.dart
+++ b/packages/flutter/test/cupertino/scrollbar_test.dart
@@ -889,4 +889,43 @@
       ),
     );
   });
+
+  testWidgets('CupertinoScrollbar scrollOrientation works correctly', (WidgetTester tester) async {
+    final ScrollController scrollController = ScrollController();
+
+    await tester.pumpWidget(
+      CupertinoApp(
+        home: PrimaryScrollController(
+          controller: scrollController,
+          child: CupertinoScrollbar(
+            isAlwaysShown: true,
+            controller: scrollController,
+            scrollbarOrientation: ScrollbarOrientation.left,
+            child: const SingleChildScrollView(
+              child: SizedBox(width: 4000.0, height: 4000.0),
+            ),
+          ),
+        ),
+      ),
+    );
+
+    await tester.pumpAndSettle();
+
+    expect(
+      find.byType(CupertinoScrollbar),
+      paints
+        ..rect(
+          rect: const Rect.fromLTRB(0.0, 0.0, 9.0, 594.0),
+        )
+        ..line(
+          p1: const Offset(0.0, 0.0),
+          p2: const Offset(0.0, 594.0),
+          strokeWidth: 1.0,
+        )
+        ..rrect(
+          rrect: RRect.fromRectAndRadius(const Rect.fromLTRB(3.0, 3.0, 6.0, 92.1), const Radius.circular(1.5)),
+          color: _kScrollbarColor.color,
+        ),
+    );
+  });
 }
diff --git a/packages/flutter/test/material/scrollbar_test.dart b/packages/flutter/test/material/scrollbar_test.dart
index f27d4f1..2a24bc7 100644
--- a/packages/flutter/test/material/scrollbar_test.dart
+++ b/packages/flutter/test/material/scrollbar_test.dart
@@ -1492,4 +1492,52 @@
       );
     }
   });
+
+  testWidgets('Scrollbar scrollOrientation works correctly', (WidgetTester tester) async {
+    final ScrollController scrollController = ScrollController();
+
+    Widget _buildScrollWithOrientation(ScrollbarOrientation orientation) {
+      return _buildBoilerplate(
+        child: Theme(
+          data: ThemeData(
+            platform: TargetPlatform.android,
+          ),
+          child: PrimaryScrollController(
+            controller: scrollController,
+            child: Scrollbar(
+              interactive: true,
+              isAlwaysShown: true,
+              scrollbarOrientation: orientation,
+              controller: scrollController,
+              child: const SingleChildScrollView(
+                child: SizedBox(width: 4000.0, height: 4000.0)
+              ),
+            ),
+          ),
+        )
+      );
+    }
+
+    await tester.pumpWidget(_buildScrollWithOrientation(ScrollbarOrientation.left));
+    await tester.pumpAndSettle();
+
+    expect(
+      find.byType(Scrollbar),
+      paints
+        ..rect(
+          rect: const Rect.fromLTRB(0.0, 0.0, 4.0, 600.0),
+          color: Colors.transparent,
+        )
+        ..line(
+          p1: const Offset(0.0, 0.0),
+          p2: const Offset(0.0, 600.0),
+          strokeWidth: 1.0,
+          color: Colors.transparent,
+        )
+        ..rect(
+          rect: const Rect.fromLTRB(0.0, 0.0, 4.0, 90.0),
+          color: _kAndroidThumbIdleColor,
+        ),
+    );
+  });
 }
diff --git a/packages/flutter/test/widgets/scrollbar_test.dart b/packages/flutter/test/widgets/scrollbar_test.dart
index 64845a6..e1ba844 100644
--- a/packages/flutter/test/widgets/scrollbar_test.dart
+++ b/packages/flutter/test/widgets/scrollbar_test.dart
@@ -26,6 +26,7 @@
   Radius? radius,
   double minLength = _kMinThumbExtent,
   double? minOverscrollLength,
+  ScrollbarOrientation? scrollbarOrientation,
   required ScrollMetrics scrollMetrics,
 }) {
   return ScrollbarPainter(
@@ -39,6 +40,7 @@
     minLength: minLength,
     minOverscrollLength: minOverscrollLength ?? minLength,
     fadeoutOpacityAnimation: kAlwaysCompleteAnimation,
+    scrollbarOrientation: scrollbarOrientation,
   )..update(scrollMetrics, scrollMetrics.axisDirection);
 }
 
@@ -248,6 +250,128 @@
     },
   );
 
+  test('scrollbarOrientation are respected', () {
+    const double viewportDimension = 23;
+    const double maxExtent = 100;
+    final ScrollMetrics startingMetrics = defaultMetrics.copyWith(
+      maxScrollExtent: maxExtent,
+      viewportDimension: viewportDimension,
+    );
+    const Size size = Size(600, viewportDimension);
+    const double margin = 0;
+
+    for (final ScrollbarOrientation scrollbarOrientation in ScrollbarOrientation.values) {
+      final AxisDirection axisDirection;
+      if (scrollbarOrientation == ScrollbarOrientation.left || scrollbarOrientation == ScrollbarOrientation.right)
+        axisDirection = AxisDirection.down;
+      else
+        axisDirection = AxisDirection.right;
+
+      painter = _buildPainter(
+        crossAxisMargin: margin,
+        scrollMetrics: startingMetrics,
+        scrollbarOrientation: scrollbarOrientation,
+      );
+
+      painter.update(
+        startingMetrics.copyWith(axisDirection: axisDirection),
+        axisDirection
+      );
+
+      painter.paint(testCanvas, size);
+      final Rect rect = captureRect();
+
+      switch (scrollbarOrientation) {
+        case ScrollbarOrientation.left:
+          expect(rect.left, 0);
+          expect(rect.top, 0);
+          expect(rect.right, _kThickness);
+          expect(rect.bottom, _kMinThumbExtent);
+          break;
+        case ScrollbarOrientation.right:
+          expect(rect.left, 600 - _kThickness);
+          expect(rect.top, 0);
+          expect(rect.right, 600);
+          expect(rect.bottom, _kMinThumbExtent);
+          break;
+        case ScrollbarOrientation.top:
+          expect(rect.left, 0);
+          expect(rect.top, 0);
+          expect(rect.right, _kMinThumbExtent);
+          expect(rect.bottom, _kThickness);
+          break;
+        case ScrollbarOrientation.bottom:
+          expect(rect.left, 0);
+          expect(rect.top, 23 - _kThickness);
+          expect(rect.right, _kMinThumbExtent);
+          expect(rect.bottom, 23);
+          break;
+      }
+    }
+  });
+
+  test('scrollbarOrientation default values are correct', () {
+    const double viewportDimension = 23;
+    const double maxExtent = 100;
+    final ScrollMetrics startingMetrics = defaultMetrics.copyWith(
+      maxScrollExtent: maxExtent,
+      viewportDimension: viewportDimension,
+    );
+    const Size size = Size(600, viewportDimension);
+    const double margin = 0;
+    Rect rect;
+
+    // Vertical scroll with TextDirection.ltr
+    painter = _buildPainter(
+      crossAxisMargin: margin,
+      scrollMetrics: startingMetrics,
+      textDirection: TextDirection.ltr,
+    );
+    painter.update(
+      startingMetrics.copyWith(axisDirection: AxisDirection.down),
+      AxisDirection.down
+    );
+    painter.paint(testCanvas, size);
+    rect = captureRect();
+    expect(rect.left, 600 - _kThickness);
+    expect(rect.top, 0);
+    expect(rect.right, 600);
+    expect(rect.bottom, _kMinThumbExtent);
+
+    // Vertical scroll with TextDirection.rtl
+    painter = _buildPainter(
+      crossAxisMargin: margin,
+      scrollMetrics: startingMetrics,
+      textDirection: TextDirection.rtl,
+    );
+    painter.update(
+      startingMetrics.copyWith(axisDirection: AxisDirection.down),
+      AxisDirection.down
+    );
+    painter.paint(testCanvas, size);
+    rect = captureRect();
+    expect(rect.left, 0);
+    expect(rect.top, 0);
+    expect(rect.right, _kThickness);
+    expect(rect.bottom, _kMinThumbExtent);
+
+    // Horizontal scroll
+    painter = _buildPainter(
+      crossAxisMargin: margin,
+      scrollMetrics: startingMetrics,
+    );
+    painter.update(
+      startingMetrics.copyWith(axisDirection: AxisDirection.right),
+      AxisDirection.right,
+    );
+    painter.paint(testCanvas, size);
+    rect = captureRect();
+    expect(rect.left, 0);
+    expect(rect.top, 23 - _kThickness);
+    expect(rect.right, _kMinThumbExtent);
+    expect(rect.bottom, 23);
+  });
+
   group('Padding works for all scroll directions', () {
     const EdgeInsets padding = EdgeInsets.fromLTRB(1, 2, 3, 4);
     const Size size = Size(60, 80);
@@ -1214,4 +1338,22 @@
         ),
     );
   });
+
+  testWidgets('ScrollbarPainter asserts if scrollbarOrientation is used with wrong axisDirection', (WidgetTester tester) async {
+    final ScrollbarPainter painter = ScrollbarPainter(
+      color: _kScrollbarColor,
+      fadeoutOpacityAnimation: kAlwaysCompleteAnimation,
+      textDirection: TextDirection.ltr,
+      scrollbarOrientation: ScrollbarOrientation.left,
+    );
+    const Size size = Size(60, 80);
+    final ScrollMetrics scrollMetrics = defaultMetrics.copyWith(
+      maxScrollExtent: 100,
+      viewportDimension: size.height,
+      axisDirection: AxisDirection.right,
+    );
+    painter.update(scrollMetrics, scrollMetrics.axisDirection);
+
+    expect(() => painter.paint(testCanvas, size), throwsA(isA<AssertionError>()));
+  });
 }