Skip to content

Commit

Permalink
feat: add YaruSplitButton
Browse files Browse the repository at this point in the history
Fixes #912
  • Loading branch information
Feichtmeier committed Oct 7, 2024
1 parent 0f213a6 commit ad49ff2
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 0 deletions.
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);
53 changes: 53 additions & 0 deletions example/lib/pages/split_button_page.dart
Original file line number Diff line number Diff line change
@@ -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'),
),
],
),
);
}
}
171 changes: 171 additions & 0 deletions lib/src/widgets/yaru_split_button.dart
Original file line number Diff line number Diff line change
@@ -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<PopupMenuEntry<Object?>> 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,
);
}
}
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 ad49ff2

Please sign in to comment.