From de3e35cb2522eb3063649004caa6e4af4445e3ba Mon Sep 17 00:00:00 2001 From: st merlhin <77164238+stMerlHin@users.noreply.github.com> Date: Wed, 12 Jul 2023 13:31:55 +0000 Subject: [PATCH] Non-scrollable `ResizablePane` (#420) --- CHANGELOG.md | 3 + example/lib/pages/resizable_pane_page.dart | 12 +- example/lib/widgets/widget_text_title1.dart | 2 +- example/pubspec.lock | 2 +- lib/src/layout/resizable_pane.dart | 62 ++++- pubspec.yaml | 2 +- test/layout/resizeable_pane_test.dart | 240 +++++++++++++++++++- test/selectors/date_picker_test.dart | 56 ++--- 8 files changed, 330 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ee2492..88c90a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.0.0-beta.9] +* `ResizablePane` can now disallow the usage of its internal scrollbar via the `ReziablePane.noScrollBar` constructor. + ## [2.0.0-beta.8] ✨ New ✨ * `MacosFontWeight` allows using Apple-specific font weights like `w510`, `w590`, and `w860`. diff --git a/example/lib/pages/resizable_pane_page.dart b/example/lib/pages/resizable_pane_page.dart index b850a0c3..78ed3320 100644 --- a/example/lib/pages/resizable_pane_page.dart +++ b/example/lib/pages/resizable_pane_page.dart @@ -73,16 +73,12 @@ class _ResizablePanePageState extends State { ); }, ), - ResizablePane( + const ResizablePane.noScrollBar( minSize: 180, startSize: 200, - // windowBreakpoint: 800, - resizableSide: ResizableSide.left, - builder: (_, __) { - return const Center( - child: Text('Right Resizable Pane'), - ); - }, + windowBreakpoint: 700, + resizableSide: ResizableSide.right, + child: Center(child: Text('Right non-scrollable Resizable Pane')), ), ], ); diff --git a/example/lib/widgets/widget_text_title1.dart b/example/lib/widgets/widget_text_title1.dart index d7dafc90..77ab7314 100644 --- a/example/lib/widgets/widget_text_title1.dart +++ b/example/lib/widgets/widget_text_title1.dart @@ -27,4 +27,4 @@ class WidgetTextTitle1 extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 6ef37937..8872a184 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -145,7 +145,7 @@ packages: path: ".." relative: true source: path - version: "2.0.0-beta.8" + version: "2.0.0-beta.9" macos_window_utils: dependency: transitive description: diff --git a/lib/src/layout/resizable_pane.dart b/lib/src/layout/resizable_pane.dart index b3f6b338..97836bdd 100644 --- a/lib/src/layout/resizable_pane.dart +++ b/lib/src/layout/resizable_pane.dart @@ -30,10 +30,15 @@ enum ResizableSide { /// The [startSize] is the initial width or height depending on the orientation of the pane. /// {@endtemplate} class ResizablePane extends StatefulWidget { - /// {@macro resizablePane} + /// Creates a [ResizablePane] with an internal [MacosScrollbar]. + /// + /// Consider using [ResizablePane.noScrollBar] constructor when the internal + /// [MacosScrollbar] is not needed or when working with widgets which do not + /// expose their scroll controllers. + /// {@macro resizablePane}. const ResizablePane({ super.key, - required this.builder, + required ScrollableWidgetBuilder this.builder, this.decoration, this.maxSize = 500.0, required this.minSize, @@ -41,7 +46,38 @@ class ResizablePane extends StatefulWidget { required this.resizableSide, this.windowBreakpoint, required this.startSize, - }) : assert( + }) : child = null, + useScrollBar = true, + assert( + maxSize >= minSize, + 'minSize should not be more than maxSize.', + ), + assert( + (startSize >= minSize) && (startSize <= maxSize), + 'startSize must not be less than minSize or more than maxWidth', + ); + + /// Creates a [ResizablePane] without an internal [MacosScrollbar]. + /// + /// Useful when working with widgets which do not expose their scroll + /// controllers or when not using the platform scroll bar is preferred. + /// + /// Consider using the default constructor if showing a [MacosScrollbar] + /// when scrolling the content of this widget is the expected behavior. + /// {@macro resizablePane}. + const ResizablePane.noScrollBar({ + super.key, + required Widget this.child, + this.decoration, + this.maxSize = 500.0, + required this.minSize, + this.isResizable = true, + required this.resizableSide, + this.windowBreakpoint, + required this.startSize, + }) : builder = null, + useScrollBar = false, + assert( maxSize >= minSize, 'minSize should not be more than maxSize.', ), @@ -55,7 +91,15 @@ class ResizablePane extends StatefulWidget { /// /// Pass the [scrollController] obtained from this method, to a scrollable /// widget used in this method to work with the internal [MacosScrollbar]. - final ScrollableWidgetBuilder builder; + final ScrollableWidgetBuilder? builder; + + /// The child to display in this widget. + /// + /// This is only referenced when the constructor used is [ResizablePane.noScrollbar]. + final Widget? child; + + /// Specify if this [ResizablePane] should have an internal [MacosScrollbar]. + final bool useScrollBar; /// The [BoxDecoration] to paint behind the child in the [builder]. final BoxDecoration? decoration; @@ -277,10 +321,12 @@ class _ResizablePaneState extends State { SafeArea( left: false, right: false, - child: MacosScrollbar( - controller: _scrollController, - child: widget.builder(context, _scrollController), - ), + child: widget.useScrollBar + ? MacosScrollbar( + controller: _scrollController, + child: widget.builder!(context, _scrollController), + ) + : widget.child!, ), if (widget.isResizable && !_resizeOnRight && !_resizeOnTop) Positioned( diff --git a/pubspec.yaml b/pubspec.yaml index 4c2877e4..c14c5d6b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: macos_ui description: Flutter widgets and themes implementing the current macOS design language. -version: 2.0.0-beta.8 +version: 2.0.0-beta.9 homepage: "https://macosui.dev" repository: "https://github.com/GroovinChip/macos_ui" diff --git a/test/layout/resizeable_pane_test.dart b/test/layout/resizeable_pane_test.dart index 56164763..f3ac9f83 100644 --- a/test/layout/resizeable_pane_test.dart +++ b/test/layout/resizeable_pane_test.dart @@ -6,6 +6,10 @@ void main() { const matrix = ResizableSide.values; group('ResizablePane', () { + const double maxSize = 300; + const double minSize = 100; + const double startSize = 200; + for (var side in matrix) { bool verticallyResizable = side == ResizableSide.top; @@ -14,10 +18,6 @@ void main() { ? 'top' : (side == ResizableSide.left ? 'left' : 'right'), () { - const double maxSize = 300; - const double minSize = 100; - const double startSize = 200; - final resizablePane = ResizablePane( builder: (context, scrollController) => const Text('Hello there'), minSize: minSize, @@ -79,6 +79,238 @@ void main() { final double safeDelta = 50.0 * directionModifier; final double overflowDelta = 500.0 * directionModifier; + testWidgets( + 'Default ResizablePane Constructor comes with an internal MacosScrollBar', + (WidgetTester tester) async { + await tester.pumpWidget(view); + expect(find.byType(MacosScrollbar), findsOneWidget); + }, + ); + + testWidgets('initial size equals startSize', (tester) async { + await tester.pumpWidget(view); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + var initialSize = verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width; + + expect(initialSize, startSize); + }); + + testWidgets('dragging wider works $side', (tester) async { + await tester.pumpWidget(view); + + await tester.drag( + dragFinder, + verticallyResizable ? Offset(0, safeDelta) : Offset(safeDelta, 0), + ); + await tester.pump(); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + expect( + verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width, + startSize + safeDelta * directionModifier, + ); + }); + + testWidgets('dragging wider respects maxSize', (tester) async { + await tester.pumpWidget(view); + + await tester.drag( + dragFinder, + verticallyResizable + ? Offset(0, overflowDelta) + : Offset(overflowDelta, 0), + ); + await tester.pump(); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + var currentSize = verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width; + expect(currentSize, maxSize); + }); + + testWidgets( + 'drag events past maxSize have no effect $side', + (tester) async { + await tester.pumpWidget(view); + + final dragStartLocation = tester.getCenter(dragFinder); + final drag = await tester.startGesture(dragStartLocation); + await drag.moveBy( + verticallyResizable + ? Offset(0, overflowDelta) + : Offset(overflowDelta, 0), + ); + await drag.moveBy( + verticallyResizable + ? Offset(0, -10.0 * directionModifier) + : Offset(-10.0 * directionModifier, 0), + ); + await drag.up(); + await tester.pump(); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + var currentSize = verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width; + expect(currentSize, maxSize); + }, + ); + + testWidgets('dragging narrower works', (tester) async { + await tester.pumpWidget(view); + + await tester.drag( + dragFinder, + verticallyResizable + ? Offset(0, -safeDelta) + : Offset(-safeDelta, 0), + ); + await tester.pump(); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + var currentSize = verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width; + expect( + currentSize, + startSize - safeDelta * directionModifier, + ); + }); + + testWidgets('dragging narrower respects minSize', (tester) async { + await tester.pumpWidget(view); + + await tester.drag( + dragFinder, + verticallyResizable + ? Offset(0, -overflowDelta) + : Offset(-overflowDelta, 0), + ); + await tester.pump(); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + var currentSize = verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width; + expect(currentSize, minSize); + }); + + testWidgets( + 'drag events past minSize have no effect', + (tester) async { + await tester.pumpWidget(view); + + final dragStartLocation = tester.getCenter(dragFinder); + final drag = await tester.startGesture(dragStartLocation); + await drag.moveBy( + verticallyResizable + ? Offset(0, -overflowDelta) + : Offset(-overflowDelta, 0), + ); + await drag.moveBy( + verticallyResizable + ? Offset(0, 10.0 * directionModifier) + : Offset(10.0 * directionModifier, 0), + ); + await drag.up(); + await tester.pump(); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + var currentSize = verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width; + expect(currentSize, minSize); + }, + ); + }, + ); + group( + side == ResizableSide.top + ? 'top' + : (side == ResizableSide.left ? 'left' : 'right'), + () { + final resizablePane = ResizablePane.noScrollBar( + minSize: minSize, + startSize: startSize, + maxSize: maxSize, + resizableSide: side, + child: const Text('Hello there'), + ); + + final view = side == ResizableSide.top + ? MacosApp( + home: MacosWindow( + disableWallpaperTinting: true, + child: MacosScaffold( + children: [ + ContentArea( + builder: (context, scrollController) { + return Column( + children: [ + const Flexible( + fit: FlexFit.loose, + child: Center( + child: Text('Hello there'), + ), + ), + resizablePane, + ], + ); + }, + ), + ], + ), + ), + ) + : MacosApp( + home: MacosWindow( + disableWallpaperTinting: true, + child: MacosScaffold( + children: [ + resizablePane, + ContentArea( + builder: (context, scrollController) { + return const Text('Hello there'); + }, + ), + ], + ), + ), + ); + + final resizablePaneFinder = find.byWidget(resizablePane); + final dragFinder = find.descendant( + of: resizablePaneFinder, + matching: find.byType(GestureDetector), + ); + + // No need to check if the resizable side is top because directionModifier + // would take -1 if it is the case + final directionModifier = side == ResizableSide.right ? 1 : -1; + final double safeDelta = 50.0 * directionModifier; + final double overflowDelta = 500.0 * directionModifier; + + testWidgets( + 'ResizablePane.noScrollBar Constructor does not come with an internal MacosScrollBar', + (WidgetTester tester) async { + await tester.pumpWidget(view); + expect(find.byType(MacosScrollbar), findsNothing); + }, + ); + testWidgets('initial size equals startSize', (tester) async { await tester.pumpWidget(view); diff --git a/test/selectors/date_picker_test.dart b/test/selectors/date_picker_test.dart index e5f4f495..f0996546 100644 --- a/test/selectors/date_picker_test.dart +++ b/test/selectors/date_picker_test.dart @@ -84,30 +84,32 @@ void main() { testWidgets( 'Textual MacosDatePicker renders the date with respect to "dateFormat" property', - (tester) async { + (tester) async { renderWidget(String dateFormat) => MacosApp( - home: MacosWindow( - disableWallpaperTinting: true, - child: MacosScaffold( - children: [ - ContentArea( - builder: (context, _) { - return Center( - child: MacosDatePicker( - initialDate: DateTime.parse('2023-04-01'), - onDateChanged: (date) {}, - dateFormat: dateFormat, - style: DatePickerStyle.textual, - ), - ); - }, + home: MacosWindow( + disableWallpaperTinting: true, + child: MacosScaffold( + children: [ + ContentArea( + builder: (context, _) { + return Center( + child: MacosDatePicker( + initialDate: DateTime.parse('2023-04-01'), + onDateChanged: (date) {}, + dateFormat: dateFormat, + style: DatePickerStyle.textual, + ), + ); + }, + ), + ], ), - ], - ), - ), - ); + ), + ); - getNthTextFromWidget(int index) => (find.byType(Text).at(index).evaluate().first.widget as Text).data as String; + getNthTextFromWidget(int index) => + (find.byType(Text).at(index).evaluate().first.widget as Text).data + as String; await tester.pumpWidget(renderWidget('dd.mm.yyyy')); String firstDateElement = getNthTextFromWidget(0); @@ -363,7 +365,7 @@ void main() { testWidgets( 'Graphical MacosDatePicker renders abbreviations based on "weekdayAbbreviations" and "monthAbbreviations" properties', - (tester) async { + (tester) async { await tester.pumpWidget( MacosApp( home: MacosWindow( @@ -424,7 +426,7 @@ void main() { testWidgets( 'Graphical MacosDatePicker with "startWeekOnMonday" set to true shows Monday as the first day of the week', - (tester) async { + (tester) async { await tester.pumpWidget( MacosApp( home: MacosWindow( @@ -453,7 +455,8 @@ void main() { matching: find.byType(Text), ); final firstWeekday = dayHeaders.first; - final firstWeekdayText = (firstWeekday.evaluate().first.widget as Text).data; + final firstWeekdayText = + (firstWeekday.evaluate().first.widget as Text).data; await tester.pumpAndSettle(); expect(firstWeekdayText, 'Mo'); @@ -474,7 +477,7 @@ void main() { // TODO: remove this once the issue is fixed and test starts failing testWidgets( 'Graphical MacosDatePicker still needs "startWeekOnMonday" to show Monday as the first day of the week, even when the locale is set to something other than "en_US"', - (tester) async { + (tester) async { await tester.pumpWidget( MacosApp( supportedLocales: const [ @@ -506,7 +509,8 @@ void main() { matching: find.byType(Text), ); final firstWeekday = dayHeaders.first; - final firstWeekdayText = (firstWeekday.evaluate().first.widget as Text).data; + final firstWeekdayText = + (firstWeekday.evaluate().first.widget as Text).data; await tester.pumpAndSettle(); // The result will be 'Tu' if the fix is no longer needed and can be removed