diff --git a/CHANGELOG.md b/CHANGELOG.md index 12e7101d..f781951c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [2.2.1] +### 🔄 Updated 🔄 +* Wrap toolbar items with `MacosToolbarPassthrough` to prevent window move or resize when interacting with toolbar items. + ## [2.2.0+3] * Address DCM lints: * Prefer `const BorderRadius.all` diff --git a/example/pubspec.lock b/example/pubspec.lock index f2be72f1..fe0fd510 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -182,7 +182,7 @@ packages: path: ".." relative: true source: path - version: "2.2.0+3" + version: "2.2.1" macos_window_utils: dependency: transitive description: diff --git a/lib/src/buttons/toolbar/toolbar_icon_button.dart b/lib/src/buttons/toolbar/toolbar_icon_button.dart index af8184d6..23abb84b 100644 --- a/lib/src/buttons/toolbar/toolbar_icon_button.dart +++ b/lib/src/buttons/toolbar/toolbar_icon_button.dart @@ -1,5 +1,6 @@ import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; +import 'package:macos_window_utils/widgets/macos_toolbar_passthrough.dart'; /// An icon button suitable for the toolbar. class ToolBarIconButton extends ToolbarItem { @@ -99,7 +100,7 @@ class ToolBarIconButton extends ToolbarItem { if (tooltipMessage != null) { iconButton = MacosTooltip(message: tooltipMessage!, child: iconButton); } - return iconButton; + return MacosToolbarPassthrough(child: iconButton); } else { return ToolbarOverflowMenuItem(label: label, onPressed: onPressed); } diff --git a/lib/src/buttons/toolbar/toolbar_pulldown_button.dart b/lib/src/buttons/toolbar/toolbar_pulldown_button.dart index 85bd87f8..64cc5e6d 100644 --- a/lib/src/buttons/toolbar/toolbar_pulldown_button.dart +++ b/lib/src/buttons/toolbar/toolbar_pulldown_button.dart @@ -1,5 +1,6 @@ import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; +import 'package:macos_window_utils/widgets/macos_toolbar_passthrough.dart'; /// A pulldown button suitable for the toolbar. class ToolBarPullDownButton extends ToolbarItem { @@ -72,7 +73,7 @@ class ToolBarPullDownButton extends ToolbarItem { child: pulldownButton, ); } - return pulldownButton; + return MacosToolbarPassthrough(child: pulldownButton); } else { // We should show a submenu for the pulldown button items. final subMenuKey = GlobalKey(); diff --git a/lib/src/layout/toolbar/custom_toolbar_item.dart b/lib/src/layout/toolbar/custom_toolbar_item.dart index 1b972934..ab90ef27 100644 --- a/lib/src/layout/toolbar/custom_toolbar_item.dart +++ b/lib/src/layout/toolbar/custom_toolbar_item.dart @@ -1,5 +1,6 @@ import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; +import 'package:macos_window_utils/widgets/macos_toolbar_passthrough.dart'; /// A custom widget for the toolbar. class CustomToolbarItem extends ToolbarItem { @@ -66,7 +67,7 @@ class CustomToolbarItem extends ToolbarItem { if (tooltipMessage != null) { widget = MacosTooltip(message: tooltipMessage!, child: widget); } - return widget; + return MacosToolbarPassthrough(child: widget); } else { return (inOverflowedBuilder != null) ? inOverflowedBuilder!(context) diff --git a/pubspec.yaml b/pubspec.yaml index e71b9ba5..be810937 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.2.0+3 +version: 2.2.1 homepage: "https://macosui.dev" repository: "https://github.com/GroovinChip/macos_ui" diff --git a/test/buttons/toolbar_icon_button_test.dart b/test/buttons/toolbar_icon_button_test.dart new file mode 100644 index 00000000..bab17458 --- /dev/null +++ b/test/buttons/toolbar_icon_button_test.dart @@ -0,0 +1,259 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_window_utils/widgets/macos_toolbar_passthrough.dart'; + +import '../mocks.dart'; + +void main() { + late MockOnPressedFunction mockOnPressedFunction; + + setUp(() { + mockOnPressedFunction = MockOnPressedFunction(); + }); + + group('ToolBarIconButton tests', () { + testWidgets( + 'ToolBarIconButton is wrapped with MacosToolbarPassthrough when in toolbar', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + ToolBarIconButton( + label: 'Add', + icon: const MacosIcon(CupertinoIcons.add), + showLabel: false, + onPressed: mockOnPressedFunction.handler, + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Verify that MacosToolbarPassthrough is present in the widget tree + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + + // Verify that the button itself is still present + expect(find.byType(MacosIconButton), findsWidgets); + }, + ); + + testWidgets( + 'ToolBarIconButton with label is wrapped with MacosToolbarPassthrough', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + ToolBarIconButton( + label: 'Add', + icon: const MacosIcon(CupertinoIcons.add), + showLabel: true, + onPressed: mockOnPressedFunction.handler, + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Verify that MacosToolbarPassthrough is present + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + + // Verify the label is displayed + expect(find.text('Add'), findsOneWidget); + }, + ); + + testWidgets( + 'ToolBarIconButton with tooltip is wrapped with MacosToolbarPassthrough', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + ToolBarIconButton( + label: 'Add', + icon: const MacosIcon(CupertinoIcons.add), + showLabel: false, + tooltipMessage: 'Add item', + onPressed: mockOnPressedFunction.handler, + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Verify that MacosToolbarPassthrough is present + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + + // Verify tooltip is present + expect(find.byType(MacosTooltip), findsOneWidget); + }, + ); + + testWidgets( + 'ToolBarIconButton still functions when wrapped with MacosToolbarPassthrough', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + ToolBarIconButton( + label: 'Add', + icon: const MacosIcon(CupertinoIcons.add), + showLabel: false, + onPressed: mockOnPressedFunction.handler, + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Verify that MacosToolbarPassthrough is present + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + + // Verify that the button is present and functional + expect(find.byType(MacosIconButton), findsWidgets); + }, + ); + + testWidgets( + 'Multiple ToolBarIconButtons each have their own MacosToolbarPassthrough', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + ToolBarIconButton( + label: 'Add', + icon: const MacosIcon(CupertinoIcons.add), + showLabel: false, + onPressed: mockOnPressedFunction.handler, + ), + ToolBarIconButton( + label: 'Remove', + icon: const MacosIcon(CupertinoIcons.minus), + showLabel: false, + onPressed: mockOnPressedFunction.handler, + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Each button should have its own MacosToolbarPassthrough wrapper + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + expect(find.byType(MacosIconButton), findsWidgets); + }, + ); + + testWidgets( + 'Disabled ToolBarIconButton is still wrapped with MacosToolbarPassthrough', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: const ToolBar( + title: Text('Test'), + actions: [ + ToolBarIconButton( + label: 'Add', + icon: MacosIcon(CupertinoIcons.add), + showLabel: false, + onPressed: null, + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Disabled button should still be wrapped + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + expect(find.byType(MacosIconButton), findsWidgets); + }, + ); + }); +} diff --git a/test/buttons/toolbar_pulldown_button_test.dart b/test/buttons/toolbar_pulldown_button_test.dart new file mode 100644 index 00000000..e4cf51e8 --- /dev/null +++ b/test/buttons/toolbar_pulldown_button_test.dart @@ -0,0 +1,303 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_window_utils/widgets/macos_toolbar_passthrough.dart'; + +import '../mocks.dart'; + +void main() { + late MockOnPressedFunction mockOnPressedFunction; + + setUp(() { + mockOnPressedFunction = MockOnPressedFunction(); + }); + + group('ToolBarPullDownButton tests', () { + testWidgets( + 'ToolBarPullDownButton is wrapped with MacosToolbarPassthrough when in toolbar', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + ToolBarPullDownButton( + label: 'Actions', + icon: CupertinoIcons.ellipsis_circle, + items: [ + MacosPulldownMenuItem( + label: 'Item 1', + title: const Text('Item 1'), + onTap: mockOnPressedFunction.handler, + ), + MacosPulldownMenuItem( + label: 'Item 2', + title: const Text('Item 2'), + onTap: mockOnPressedFunction.handler, + ), + ], + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Verify that MacosToolbarPassthrough is present in the widget tree + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + + // Verify that the pulldown button itself is still present + expect(find.byType(MacosPulldownButton), findsOneWidget); + }, + ); + + testWidgets( + 'ToolBarPullDownButton with tooltip is wrapped with MacosToolbarPassthrough', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + ToolBarPullDownButton( + label: 'Actions', + icon: CupertinoIcons.ellipsis_circle, + tooltipMessage: 'More actions', + items: [ + MacosPulldownMenuItem( + label: 'Item 1', + title: const Text('Item 1'), + onTap: mockOnPressedFunction.handler, + ), + ], + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Verify that MacosToolbarPassthrough is present + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + + // Verify tooltip is present + expect(find.byType(MacosTooltip), findsOneWidget); + }, + ); + + testWidgets( + 'ToolBarPullDownButton still functions when wrapped with MacosToolbarPassthrough', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + ToolBarPullDownButton( + label: 'Actions', + icon: CupertinoIcons.ellipsis_circle, + items: [ + MacosPulldownMenuItem( + label: 'Item 1', + title: const Text('Item 1'), + onTap: mockOnPressedFunction.handler, + ), + MacosPulldownMenuItem( + label: 'Item 2', + title: const Text('Item 2'), + onTap: mockOnPressedFunction.handler, + ), + ], + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Verify that MacosToolbarPassthrough is present + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + + // Verify that the pulldown button is present and functional + expect(find.byType(MacosPulldownButton), findsWidgets); + }, + ); + + testWidgets( + 'Disabled ToolBarPullDownButton is still wrapped with MacosToolbarPassthrough', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: const ToolBar( + title: Text('Test'), + actions: [ + ToolBarPullDownButton( + label: 'Actions', + icon: CupertinoIcons.ellipsis_circle, + items: null, + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Disabled pulldown button should still be wrapped + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + expect(find.byType(MacosPulldownButton), findsOneWidget); + }, + ); + + testWidgets( + 'Multiple ToolBarPullDownButtons each have their own MacosToolbarPassthrough', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + ToolBarPullDownButton( + label: 'Actions', + icon: CupertinoIcons.ellipsis_circle, + items: [ + MacosPulldownMenuItem( + label: 'Item 1', + title: const Text('Item 1'), + onTap: mockOnPressedFunction.handler, + ), + ], + ), + ToolBarPullDownButton( + label: 'More', + icon: CupertinoIcons.gear, + items: [ + MacosPulldownMenuItem( + label: 'Item 2', + title: const Text('Item 2'), + onTap: mockOnPressedFunction.handler, + ), + ], + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Each pulldown button should have its own MacosToolbarPassthrough wrapper + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + expect(find.byType(MacosPulldownButton), findsWidgets); + }, + ); + + testWidgets( + 'Mixed toolbar with icon and pulldown buttons have correct MacosToolbarPassthrough wrappers', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + ToolBarIconButton( + label: 'Add', + icon: const MacosIcon(CupertinoIcons.add), + showLabel: false, + onPressed: mockOnPressedFunction.handler, + ), + ToolBarPullDownButton( + label: 'Actions', + icon: CupertinoIcons.ellipsis_circle, + items: [ + MacosPulldownMenuItem( + label: 'Item 1', + title: const Text('Item 1'), + onTap: mockOnPressedFunction.handler, + ), + ], + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Both items should be wrapped with MacosToolbarPassthrough + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + expect(find.byType(MacosIconButton), findsWidgets); + expect(find.byType(MacosPulldownButton), findsWidgets); + }, + ); + }); +} diff --git a/test/layout/custom_toolbar_item_test.dart b/test/layout/custom_toolbar_item_test.dart new file mode 100644 index 00000000..5c8bd1b1 --- /dev/null +++ b/test/layout/custom_toolbar_item_test.dart @@ -0,0 +1,291 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_window_utils/widgets/macos_toolbar_passthrough.dart'; + +import '../mocks.dart'; + +void main() { + late MockOnPressedFunction mockOnPressedFunction; + + setUp(() { + mockOnPressedFunction = MockOnPressedFunction(); + }); + + group('CustomToolbarItem tests', () { + testWidgets( + 'CustomToolbarItem is wrapped with MacosToolbarPassthrough when in toolbar', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + CustomToolbarItem( + inToolbarBuilder: (context) => Container( + width: 100, + height: 30, + color: CupertinoColors.systemBlue, + child: const Center(child: Text('Custom Widget')), + ), + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Verify that MacosToolbarPassthrough is present in the widget tree + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + + // Verify that the custom widget is still present + expect(find.text('Custom Widget'), findsOneWidget); + }, + ); + + testWidgets( + 'CustomToolbarItem with MacosSearchField is wrapped with MacosToolbarPassthrough', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + CustomToolbarItem( + inToolbarBuilder: (context) => + const SizedBox(width: 200, child: MacosSearchField()), + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Verify that MacosToolbarPassthrough is present + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + + // Verify that the search field is present + expect(find.byType(MacosSearchField), findsOneWidget); + }, + ); + + testWidgets( + 'CustomToolbarItem with tooltip is wrapped with MacosToolbarPassthrough', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + CustomToolbarItem( + tooltipMessage: 'Custom Tooltip', + inToolbarBuilder: (context) => Container( + width: 100, + height: 30, + color: CupertinoColors.systemBlue, + child: const Center(child: Text('Custom Widget')), + ), + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Verify that MacosToolbarPassthrough is present + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + + // Verify tooltip is present + expect(find.byType(MacosTooltip), findsOneWidget); + }, + ); + + testWidgets('CustomToolbarItem interaction still works when wrapped', ( + tester, + ) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + CustomToolbarItem( + inToolbarBuilder: (context) => GestureDetector( + onTap: mockOnPressedFunction.handler, + child: Container( + width: 100, + height: 30, + color: CupertinoColors.systemBlue, + child: const Center(child: Text('Tap Me')), + ), + ), + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Find and tap the custom widget + final customWidget = find.text('Tap Me'); + expect(customWidget, findsOneWidget); + + await tester.tap(customWidget); + await tester.pumpAndSettle(); + + // Verify the callback was called + expect(mockOnPressedFunction.called, greaterThan(0)); + }); + + testWidgets( + 'Multiple CustomToolbarItems each have their own MacosToolbarPassthrough', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + CustomToolbarItem( + inToolbarBuilder: (context) => Container( + width: 100, + height: 30, + color: CupertinoColors.systemBlue, + child: const Center(child: Text('Custom 1')), + ), + ), + CustomToolbarItem( + inToolbarBuilder: (context) => Container( + width: 100, + height: 30, + color: CupertinoColors.systemGreen, + child: const Center(child: Text('Custom 2')), + ), + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Each custom item should have its own MacosToolbarPassthrough wrapper + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + expect(find.text('Custom 1'), findsOneWidget); + expect(find.text('Custom 2'), findsOneWidget); + }, + ); + + testWidgets( + 'Mixed toolbar with custom, icon, and pulldown items all have MacosToolbarPassthrough', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + toolBar: ToolBar( + title: const Text('Test'), + actions: [ + ToolBarIconButton( + label: 'Add', + icon: const MacosIcon(CupertinoIcons.add), + showLabel: false, + onPressed: mockOnPressedFunction.handler, + ), + CustomToolbarItem( + inToolbarBuilder: (context) => + const SizedBox(width: 150, child: MacosSearchField()), + ), + ToolBarPullDownButton( + label: 'Actions', + icon: CupertinoIcons.ellipsis_circle, + items: [ + MacosPulldownMenuItem( + label: 'Item 1', + title: const Text('Item 1'), + onTap: mockOnPressedFunction.handler, + ), + ], + ), + ], + ), + children: [ + ContentArea( + builder: (context, _) { + return const Center(child: Text('Content')); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // All three toolbar items should be wrapped with MacosToolbarPassthrough + expect(find.byType(MacosToolbarPassthrough), findsWidgets); + expect(find.byType(MacosIconButton), findsWidgets); + expect(find.byType(MacosSearchField), findsWidgets); + expect(find.byType(MacosPulldownButton), findsWidgets); + }, + ); + }); +}