From ad49ff216ac0738a226382c39ab1e0feb26947da Mon Sep 17 00:00:00 2001 From: Feichtmeier Date: Mon, 7 Oct 2024 17:01:03 +0200 Subject: [PATCH] feat: add YaruSplitButton Fixes #912 --- example/lib/example_page_items.dart | 10 ++ example/lib/pages/split_button_page.dart | 53 +++++++ lib/src/widgets/yaru_split_button.dart | 171 +++++++++++++++++++++++ lib/widgets.dart | 1 + pubspec.yaml | 1 + 5 files changed, 236 insertions(+) create mode 100644 example/lib/pages/split_button_page.dart create mode 100644 lib/src/widgets/yaru_split_button.dart diff --git a/example/lib/example_page_items.dart b/example/lib/example_page_items.dart index f8856de0..dd439b19 100644 --- a/example/lib/example_page_items.dart +++ b/example/lib/example_page_items.dart @@ -30,6 +30,7 @@ import 'pages/radio_page.dart'; import 'pages/search_field_page.dart'; import 'pages/section_page.dart'; import 'pages/selectable_container_page.dart'; +import 'pages/split_button_page.dart'; import 'pages/switch_page.dart'; import 'pages/tab_bar_page.dart'; import 'pages/theme_page/theme_page.dart'; @@ -381,4 +382,13 @@ final examplePageItems = [ 'https://raw.githubusercontent.com/ubuntu/yaru.dart/main/example/lib/pages/border_container_page.dart', ), ), + PageItem( + title: 'YaruSplitButton', + floatingActionButtonBuilder: (_) => const CodeSnippedButton( + snippetUrl: + 'https://raw.githubusercontent.com/ubuntu/yaru.dart/main/example/lib/pages/split_button_page.dart', + ), + pageBuilder: (context) => const SplitButtonPage(), + iconBuilder: (context, selected) => const Icon(YaruIcons.pan_down), + ), ].sortedBy((page) => page.title); diff --git a/example/lib/pages/split_button_page.dart b/example/lib/pages/split_button_page.dart new file mode 100644 index 00000000..f0f20702 --- /dev/null +++ b/example/lib/pages/split_button_page.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:yaru/yaru.dart'; + +class SplitButtonPage extends StatelessWidget { + const SplitButtonPage({super.key}); + + @override + Widget build(BuildContext context) { + final items = List.generate( + 10, + (index) { + final text = + '${index.isEven ? 'Super long action name' : 'action'} ${index + 1}'; + return PopupMenuItem( + child: Text( + text, + overflow: TextOverflow.ellipsis, + ), + onTap: () => ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(text))), + ); + }, + ); + + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + YaruSplitButton( + onPressed: () => ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Main Action'))), + items: items, + child: const Text('Main Action'), + ), + const SizedBox(height: 10), + YaruSplitButton.filled( + onPressed: () => ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Main Action'))), + items: items, + child: const Text('Main Action'), + ), + const SizedBox(height: 10), + YaruSplitButton.outlined( + onPressed: () => ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Main Action'))), + items: items, + child: const Text('Main Action'), + ), + ], + ), + ); + } +} diff --git a/lib/src/widgets/yaru_split_button.dart b/lib/src/widgets/yaru_split_button.dart new file mode 100644 index 00000000..0ef5e7a3 --- /dev/null +++ b/lib/src/widgets/yaru_split_button.dart @@ -0,0 +1,171 @@ +import 'package:assorted_layout_widgets/assorted_layout_widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:yaru/yaru.dart'; + +enum _YaruSplitButtonVariant { elevated, filled, outlined } + +class YaruSplitButton extends StatelessWidget { + const YaruSplitButton({ + super.key, + required this.items, + this.onPressed, + this.child, + this.onOptionsPressed, + this.icon, + this.radius, + this.menuWidth = menuDefaultWidth, + }) : _variant = _YaruSplitButtonVariant.elevated; + + const YaruSplitButton.filled({ + super.key, + required this.items, + this.onPressed, + this.child, + this.onOptionsPressed, + this.icon, + this.radius, + this.menuWidth = menuDefaultWidth, + }) : _variant = _YaruSplitButtonVariant.filled; + + const YaruSplitButton.outlined({ + super.key, + required this.items, + this.onPressed, + this.child, + this.onOptionsPressed, + this.icon, + this.radius, + this.menuWidth = menuDefaultWidth, + }) : _variant = _YaruSplitButtonVariant.outlined; + + final _YaruSplitButtonVariant _variant; + final void Function()? onPressed; + final void Function()? onOptionsPressed; + final Widget? child; + final Widget? icon; + final List> items; + final double? radius; + final double menuWidth; + + static const menuDefaultWidth = 148.0; + + @override + Widget build(BuildContext context) { + // TODO: fix common_themes to use a fixed size for buttons instead of fiddling around with padding + // then we can rely on this size here + const size = Size.square(36); + const dropdownPadding = EdgeInsets.only(top: 16, bottom: 16); + + final defaultRadius = Radius.circular(radius ?? kYaruButtonRadius); + + final mainActionShape = RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: defaultRadius, + bottomLeft: defaultRadius, + ), + ); + + final dropdownShape = switch (_variant) { + _YaruSplitButtonVariant.outlined => + const NonUniformRoundedRectangleBorder(hideLeftSide: true), + _ => RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: defaultRadius, + bottomRight: defaultRadius, + ), + ), + }; + + final onDropdownPressed = onPressed == null + ? null + : (onOptionsPressed ?? + () => showMenu( + context: context, + position: _menuPosition(context), + items: items, + menuPadding: EdgeInsets.symmetric(vertical: defaultRadius.x), + constraints: BoxConstraints( + minWidth: menuWidth, + maxWidth: menuWidth, + ), + )); + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + switch (_variant) { + _YaruSplitButtonVariant.elevated => ElevatedButton( + style: ElevatedButton.styleFrom(shape: mainActionShape), + onPressed: onPressed, + child: child, + ), + _YaruSplitButtonVariant.filled => FilledButton( + style: FilledButton.styleFrom(shape: mainActionShape), + onPressed: onPressed, + child: child, + ), + _YaruSplitButtonVariant.outlined => OutlinedButton( + style: OutlinedButton.styleFrom(shape: mainActionShape), + onPressed: onPressed, + child: child, + ), + }, + switch (_variant) { + _YaruSplitButtonVariant.elevated => ElevatedButton( + style: ElevatedButton.styleFrom( + fixedSize: size, + minimumSize: size, + maximumSize: size, + padding: dropdownPadding, + shape: dropdownShape, + ), + onPressed: onDropdownPressed, + child: icon ?? const Icon(YaruIcons.pan_down), + ), + _YaruSplitButtonVariant.filled => FilledButton( + style: FilledButton.styleFrom( + fixedSize: size, + minimumSize: size, + maximumSize: size, + padding: dropdownPadding, + shape: dropdownShape, + ), + onPressed: onDropdownPressed, + child: icon ?? const Icon(YaruIcons.pan_down), + ), + _YaruSplitButtonVariant.outlined => OutlinedButton( + style: OutlinedButton.styleFrom( + fixedSize: size, + minimumSize: size, + maximumSize: size, + padding: dropdownPadding, + shape: dropdownShape, + ), + onPressed: onDropdownPressed, + child: icon ?? const Icon(YaruIcons.pan_down), + ), + }, + ], + ); + } + + RelativeRect _menuPosition(BuildContext context) { + final bar = context.findRenderObject() as RenderBox; + final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + const offset = Offset.zero; + + return RelativeRect.fromRect( + Rect.fromPoints( + bar.localToGlobal( + bar.size.bottomCenter(offset), + ancestor: overlay, + ), + bar.localToGlobal( + bar.size.bottomLeft(offset), + ancestor: overlay, + ), + ), + offset & overlay.size, + ); + } +} diff --git a/lib/widgets.dart b/lib/widgets.dart index 27159420..f2861fb9 100644 --- a/lib/widgets.dart +++ b/lib/widgets.dart @@ -43,6 +43,7 @@ export 'src/widgets/yaru_search_field.dart'; export 'src/widgets/yaru_section.dart'; export 'src/widgets/yaru_segmented_entry.dart'; export 'src/widgets/yaru_selectable_container.dart'; +export 'src/widgets/yaru_split_button.dart'; export 'src/widgets/yaru_switch.dart'; export 'src/widgets/yaru_switch_button.dart'; export 'src/widgets/yaru_switch_list_tile.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 0aae8923..2b4e7dbb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ environment: dependencies: animated_vector: ^0.2.0 animated_vector_annotations: ^0.2.0 + assorted_layout_widgets: ^9.0.2 collection: ^1.17.0 dbus: ^0.7.10 flutter: