Skip to content

Commit

Permalink
feat: YaruSplitButton (#928)
Browse files Browse the repository at this point in the history
* feat: add YaruSplitButton

Ref #912
  • Loading branch information
Feichtmeier authored Oct 9, 2024
1 parent 0f213a6 commit 9bfae97
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 0 deletions.
20 changes: 20 additions & 0 deletions example/lib/common/space.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:flutter/widgets.dart';

List<Widget> space({
required Iterable<Widget> children,
double? widthGap,
double? heightGap,
int skip = 1,
}) =>
children
.expand(
(item) sync* {
yield SizedBox(
width: widthGap,
height: heightGap,
);
yield item;
},
)
.skip(skip)
.toList();
10 changes: 10 additions & 0 deletions example/lib/example_page_items.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -381,4 +382,13 @@ final examplePageItems = <PageItem>[
'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);
85 changes: 85 additions & 0 deletions example/lib/pages/split_button_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:yaru/yaru.dart';

import '../common/space.dart';

class SplitButtonPage extends StatefulWidget {
const SplitButtonPage({super.key});

@override
State<SplitButtonPage> createState() => _SplitButtonPageState();
}

class _SplitButtonPageState extends State<SplitButtonPage> {
double width = 200.0;

@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: space(
heightGap: 10,
children: [
YaruSplitButton(
onPressed: () => ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Main Action'))),
items: items,
child: const Text('Main Action'),
),
YaruSplitButton.filled(
onPressed: () => ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Main Action'))),
items: items,
child: const Text('Main Action'),
),
YaruSplitButton.outlined(
menuWidth: width,
onPressed: () => ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Main Action'))),
items: items,
child: const Text('Main Action'),
),
SizedBox(
width: 300,
child: Slider(
min: 100,
max: 500,
value: width,
onChanged: (v) => setState(() => width = v),
),
),
Center(
child: Text('Menu width: ${width.toInt()}'),
),
YaruSplitButton(
menuWidth: width,
items: items,
child: const Text('Main Action'),
),
YaruSplitButton(
menuWidth: width,
child: const Text('Main Action'),
),
],
),
),
);
}
}
177 changes: 177 additions & 0 deletions lib/src/widgets/yaru_split_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
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,
this.items,
this.onPressed,
this.child,
this.onOptionsPressed,
this.icon,
this.radius,
this.menuWidth,
}) : _variant = _YaruSplitButtonVariant.elevated;

const YaruSplitButton.filled({
super.key,
this.items,
this.onPressed,
this.child,
this.onOptionsPressed,
this.icon,
this.radius,
this.menuWidth,
}) : _variant = _YaruSplitButtonVariant.filled;

const YaruSplitButton.outlined({
super.key,
this.items,
this.onPressed,
this.child,
this.onOptionsPressed,
this.icon,
this.radius,
this.menuWidth,
}) : _variant = _YaruSplitButtonVariant.outlined;

final _YaruSplitButtonVariant _variant;
final void Function()? onPressed;
final void Function()? onOptionsPressed;
final Widget? child;
final Widget? icon;
final List<PopupMenuEntry<Object?>>? items;
final double? radius;
final double? menuWidth;

@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 => NonUniformRoundedRectangleBorder(
hideLeftSide: true,
borderRadius: BorderRadius.all(
defaultRadius,
),
),
_ => RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: defaultRadius,
bottomRight: defaultRadius,
),
),
};

final onDropdownPressed = onOptionsPressed ??
(items?.isNotEmpty == true
? () => showMenu(
context: context,
position: _menuPosition(context),
items: items!,
menuPadding: EdgeInsets.symmetric(vertical: defaultRadius.x),
constraints: menuWidth == null
? null
: BoxConstraints(
minWidth: menuWidth!,
maxWidth: menuWidth!,
),
)
: null);

final dropdownIcon = icon ?? const Icon(YaruIcons.pan_down);

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: dropdownIcon,
),
_YaruSplitButtonVariant.filled => FilledButton(
style: FilledButton.styleFrom(
fixedSize: size,
minimumSize: size,
maximumSize: size,
padding: dropdownPadding,
shape: dropdownShape,
),
onPressed: onDropdownPressed,
child: dropdownIcon,
),
_YaruSplitButtonVariant.outlined => OutlinedButton(
style: OutlinedButton.styleFrom(
fixedSize: size,
minimumSize: size,
maximumSize: size,
padding: dropdownPadding,
shape: dropdownShape,
),
onPressed: onDropdownPressed,
child: dropdownIcon,
),
},
],
);
}

RelativeRect _menuPosition(BuildContext context) {
final box = context.findRenderObject() as RenderBox;
final overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
const offset = Offset.zero;

return RelativeRect.fromRect(
Rect.fromPoints(
box.localToGlobal(
box.size.bottomCenter(offset),
ancestor: overlay,
),
box.localToGlobal(
box.size.bottomRight(offset),
ancestor: overlay,
),
),
offset & overlay.size,
);
}
}
1 change: 1 addition & 0 deletions lib/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 9bfae97

Please sign in to comment.