Skip to content

Commit ad49ff2

Browse files
committed
feat: add YaruSplitButton
Fixes #912
1 parent 0f213a6 commit ad49ff2

File tree

5 files changed

+236
-0
lines changed

5 files changed

+236
-0
lines changed

example/lib/example_page_items.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import 'pages/radio_page.dart';
3030
import 'pages/search_field_page.dart';
3131
import 'pages/section_page.dart';
3232
import 'pages/selectable_container_page.dart';
33+
import 'pages/split_button_page.dart';
3334
import 'pages/switch_page.dart';
3435
import 'pages/tab_bar_page.dart';
3536
import 'pages/theme_page/theme_page.dart';
@@ -381,4 +382,13 @@ final examplePageItems = <PageItem>[
381382
'https://raw.githubusercontent.com/ubuntu/yaru.dart/main/example/lib/pages/border_container_page.dart',
382383
),
383384
),
385+
PageItem(
386+
title: 'YaruSplitButton',
387+
floatingActionButtonBuilder: (_) => const CodeSnippedButton(
388+
snippetUrl:
389+
'https://raw.githubusercontent.com/ubuntu/yaru.dart/main/example/lib/pages/split_button_page.dart',
390+
),
391+
pageBuilder: (context) => const SplitButtonPage(),
392+
iconBuilder: (context, selected) => const Icon(YaruIcons.pan_down),
393+
),
384394
].sortedBy((page) => page.title);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:yaru/yaru.dart';
3+
4+
class SplitButtonPage extends StatelessWidget {
5+
const SplitButtonPage({super.key});
6+
7+
@override
8+
Widget build(BuildContext context) {
9+
final items = List.generate(
10+
10,
11+
(index) {
12+
final text =
13+
'${index.isEven ? 'Super long action name' : 'action'} ${index + 1}';
14+
return PopupMenuItem(
15+
child: Text(
16+
text,
17+
overflow: TextOverflow.ellipsis,
18+
),
19+
onTap: () => ScaffoldMessenger.of(context)
20+
.showSnackBar(SnackBar(content: Text(text))),
21+
);
22+
},
23+
);
24+
25+
return Center(
26+
child: Column(
27+
mainAxisSize: MainAxisSize.min,
28+
children: [
29+
YaruSplitButton(
30+
onPressed: () => ScaffoldMessenger.of(context)
31+
.showSnackBar(const SnackBar(content: Text('Main Action'))),
32+
items: items,
33+
child: const Text('Main Action'),
34+
),
35+
const SizedBox(height: 10),
36+
YaruSplitButton.filled(
37+
onPressed: () => ScaffoldMessenger.of(context)
38+
.showSnackBar(const SnackBar(content: Text('Main Action'))),
39+
items: items,
40+
child: const Text('Main Action'),
41+
),
42+
const SizedBox(height: 10),
43+
YaruSplitButton.outlined(
44+
onPressed: () => ScaffoldMessenger.of(context)
45+
.showSnackBar(const SnackBar(content: Text('Main Action'))),
46+
items: items,
47+
child: const Text('Main Action'),
48+
),
49+
],
50+
),
51+
);
52+
}
53+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import 'package:assorted_layout_widgets/assorted_layout_widgets.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:yaru/yaru.dart';
4+
5+
enum _YaruSplitButtonVariant { elevated, filled, outlined }
6+
7+
class YaruSplitButton extends StatelessWidget {
8+
const YaruSplitButton({
9+
super.key,
10+
required this.items,
11+
this.onPressed,
12+
this.child,
13+
this.onOptionsPressed,
14+
this.icon,
15+
this.radius,
16+
this.menuWidth = menuDefaultWidth,
17+
}) : _variant = _YaruSplitButtonVariant.elevated;
18+
19+
const YaruSplitButton.filled({
20+
super.key,
21+
required this.items,
22+
this.onPressed,
23+
this.child,
24+
this.onOptionsPressed,
25+
this.icon,
26+
this.radius,
27+
this.menuWidth = menuDefaultWidth,
28+
}) : _variant = _YaruSplitButtonVariant.filled;
29+
30+
const YaruSplitButton.outlined({
31+
super.key,
32+
required this.items,
33+
this.onPressed,
34+
this.child,
35+
this.onOptionsPressed,
36+
this.icon,
37+
this.radius,
38+
this.menuWidth = menuDefaultWidth,
39+
}) : _variant = _YaruSplitButtonVariant.outlined;
40+
41+
final _YaruSplitButtonVariant _variant;
42+
final void Function()? onPressed;
43+
final void Function()? onOptionsPressed;
44+
final Widget? child;
45+
final Widget? icon;
46+
final List<PopupMenuEntry<Object?>> items;
47+
final double? radius;
48+
final double menuWidth;
49+
50+
static const menuDefaultWidth = 148.0;
51+
52+
@override
53+
Widget build(BuildContext context) {
54+
// TODO: fix common_themes to use a fixed size for buttons instead of fiddling around with padding
55+
// then we can rely on this size here
56+
const size = Size.square(36);
57+
const dropdownPadding = EdgeInsets.only(top: 16, bottom: 16);
58+
59+
final defaultRadius = Radius.circular(radius ?? kYaruButtonRadius);
60+
61+
final mainActionShape = RoundedRectangleBorder(
62+
borderRadius: BorderRadius.only(
63+
topLeft: defaultRadius,
64+
bottomLeft: defaultRadius,
65+
),
66+
);
67+
68+
final dropdownShape = switch (_variant) {
69+
_YaruSplitButtonVariant.outlined =>
70+
const NonUniformRoundedRectangleBorder(hideLeftSide: true),
71+
_ => RoundedRectangleBorder(
72+
borderRadius: BorderRadius.only(
73+
topRight: defaultRadius,
74+
bottomRight: defaultRadius,
75+
),
76+
),
77+
};
78+
79+
final onDropdownPressed = onPressed == null
80+
? null
81+
: (onOptionsPressed ??
82+
() => showMenu(
83+
context: context,
84+
position: _menuPosition(context),
85+
items: items,
86+
menuPadding: EdgeInsets.symmetric(vertical: defaultRadius.x),
87+
constraints: BoxConstraints(
88+
minWidth: menuWidth,
89+
maxWidth: menuWidth,
90+
),
91+
));
92+
93+
return Row(
94+
mainAxisSize: MainAxisSize.min,
95+
children: [
96+
switch (_variant) {
97+
_YaruSplitButtonVariant.elevated => ElevatedButton(
98+
style: ElevatedButton.styleFrom(shape: mainActionShape),
99+
onPressed: onPressed,
100+
child: child,
101+
),
102+
_YaruSplitButtonVariant.filled => FilledButton(
103+
style: FilledButton.styleFrom(shape: mainActionShape),
104+
onPressed: onPressed,
105+
child: child,
106+
),
107+
_YaruSplitButtonVariant.outlined => OutlinedButton(
108+
style: OutlinedButton.styleFrom(shape: mainActionShape),
109+
onPressed: onPressed,
110+
child: child,
111+
),
112+
},
113+
switch (_variant) {
114+
_YaruSplitButtonVariant.elevated => ElevatedButton(
115+
style: ElevatedButton.styleFrom(
116+
fixedSize: size,
117+
minimumSize: size,
118+
maximumSize: size,
119+
padding: dropdownPadding,
120+
shape: dropdownShape,
121+
),
122+
onPressed: onDropdownPressed,
123+
child: icon ?? const Icon(YaruIcons.pan_down),
124+
),
125+
_YaruSplitButtonVariant.filled => FilledButton(
126+
style: FilledButton.styleFrom(
127+
fixedSize: size,
128+
minimumSize: size,
129+
maximumSize: size,
130+
padding: dropdownPadding,
131+
shape: dropdownShape,
132+
),
133+
onPressed: onDropdownPressed,
134+
child: icon ?? const Icon(YaruIcons.pan_down),
135+
),
136+
_YaruSplitButtonVariant.outlined => OutlinedButton(
137+
style: OutlinedButton.styleFrom(
138+
fixedSize: size,
139+
minimumSize: size,
140+
maximumSize: size,
141+
padding: dropdownPadding,
142+
shape: dropdownShape,
143+
),
144+
onPressed: onDropdownPressed,
145+
child: icon ?? const Icon(YaruIcons.pan_down),
146+
),
147+
},
148+
],
149+
);
150+
}
151+
152+
RelativeRect _menuPosition(BuildContext context) {
153+
final bar = context.findRenderObject() as RenderBox;
154+
final overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
155+
const offset = Offset.zero;
156+
157+
return RelativeRect.fromRect(
158+
Rect.fromPoints(
159+
bar.localToGlobal(
160+
bar.size.bottomCenter(offset),
161+
ancestor: overlay,
162+
),
163+
bar.localToGlobal(
164+
bar.size.bottomLeft(offset),
165+
ancestor: overlay,
166+
),
167+
),
168+
offset & overlay.size,
169+
);
170+
}
171+
}

lib/widgets.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export 'src/widgets/yaru_search_field.dart';
4343
export 'src/widgets/yaru_section.dart';
4444
export 'src/widgets/yaru_segmented_entry.dart';
4545
export 'src/widgets/yaru_selectable_container.dart';
46+
export 'src/widgets/yaru_split_button.dart';
4647
export 'src/widgets/yaru_switch.dart';
4748
export 'src/widgets/yaru_switch_button.dart';
4849
export 'src/widgets/yaru_switch_list_tile.dart';

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ environment:
1111
dependencies:
1212
animated_vector: ^0.2.0
1313
animated_vector_annotations: ^0.2.0
14+
assorted_layout_widgets: ^9.0.2
1415
collection: ^1.17.0
1516
dbus: ^0.7.10
1617
flutter:

0 commit comments

Comments
 (0)