Toggle Group
Toggles joined into one piece, one or several pressed.
import 'package:ashlar_icons/ashlar_icons.dart';
import 'package:flutter/widgets.dart';
import 'ui/toggle_group.dart';
class ToggleGroupDemo extends StatefulWidget {
const ToggleGroupDemo({super.key});
@override
State<ToggleGroupDemo> createState() => _ToggleGroupDemoState();
}
class _ToggleGroupDemoState extends State<ToggleGroupDemo> {
Set<String> styles = {'bold'};
Set<String> align = {'left'};
@override
Widget build(BuildContext context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ToggleGroup<String>(
multiple: true,
semanticLabel: 'Text style',
values: styles,
onChanged: (v) => setState(() => styles = v),
items: const [
ToggleGroupItem('bold', AshlarIcon(AshlarIconsRoundedStroke.bold), semanticLabel: 'Bold'),
ToggleGroupItem('italic', AshlarIcon(AshlarIconsRoundedStroke.italic), semanticLabel: 'Italic'),
ToggleGroupItem('underline', AshlarIcon(AshlarIconsRoundedStroke.underline), semanticLabel: 'Underline'),
],
),
const SizedBox(height: 16),
ToggleGroup<String>(
semanticLabel: 'Alignment',
values: align,
onChanged: (v) => setState(() => align = v),
items: const [
ToggleGroupItem('left', AshlarIcon(AshlarIconsRoundedStroke.textAlignStart), semanticLabel: 'Align left'),
ToggleGroupItem('center', AshlarIcon(AshlarIconsRoundedStroke.textAlignCenter), semanticLabel: 'Centre'),
ToggleGroupItem('right', AshlarIcon(AshlarIconsRoundedStroke.textAlignEnd), semanticLabel: 'Align right'),
],
),
],
);
}
Try its options
Each option its constructor takes, to change and see; the code below it is what you chose, to copy.
Add it
In your Flutter project:
ashlar add toggle-group
It writes theme.dart, palette.dart, group.dart, pressable.dart, toggle.dart and toggle_group.dart into lib/ui, keeping any you already have. It needs toggle and group, which come with it.
Examples
Other ways to use it, each running live, with its code.
Several at once
Bold, italic and underline together.
// Several at once: Bold, italic and underline together.
import 'package:ashlar_icons/ashlar_icons.dart';
import 'package:flutter/widgets.dart';
import 'ui/toggle_group.dart';
class ToggleGroupMultipleExample extends StatefulWidget {
const ToggleGroupMultipleExample({super.key});
@override
State<ToggleGroupMultipleExample> createState() => _ToggleGroupMultipleExampleState();
}
class _ToggleGroupMultipleExampleState extends State<ToggleGroupMultipleExample> {
var on = {'bold'};
@override
Widget build(BuildContext context) => ToggleGroup<String>(
multiple: true,
values: on,
semanticLabel: 'Text style',
onChanged: (v) => setState(() => on = v),
items: const [
ToggleGroupItem('bold', AshlarIcon(AshlarIconsRoundedStroke.bold), semanticLabel: 'Bold'),
ToggleGroupItem('italic', AshlarIcon(AshlarIconsRoundedStroke.italic), semanticLabel: 'Italic'),
ToggleGroupItem('underline', AshlarIcon(AshlarIconsRoundedStroke.underline), semanticLabel: 'Underline'),
],
);
}
Spaced
Apart, each with its own border.
// Spaced: Apart, each with its own border.
import 'package:flutter/widgets.dart';
import 'ui/toggle_group.dart';
class ToggleGroupSpacedExample extends StatefulWidget {
const ToggleGroupSpacedExample({super.key});
@override
State<ToggleGroupSpacedExample> createState() => _ToggleGroupSpacedExampleState();
}
class _ToggleGroupSpacedExampleState extends State<ToggleGroupSpacedExample> {
var view = 'week';
@override
Widget build(BuildContext context) => ToggleGroup<String>(
spacing: 8,
values: {view},
onChanged: (v) => setState(() => view = v.firstOrNull ?? view),
items: const [
ToggleGroupItem('day', Text('Day')),
ToggleGroupItem('week', Text('Week')),
ToggleGroupItem('month', Text('Month')),
],
);
}
Upright
Stacked, the arrows moving up and down.
// Upright: Stacked, the arrows moving up and down.
import 'package:flutter/widgets.dart';
import 'ui/toggle_group.dart';
class ToggleGroupVerticalExample extends StatefulWidget {
const ToggleGroupVerticalExample({super.key});
@override
State<ToggleGroupVerticalExample> createState() => _ToggleGroupVerticalExampleState();
}
class _ToggleGroupVerticalExampleState extends State<ToggleGroupVerticalExample> {
var align = 'top';
@override
Widget build(BuildContext context) => ToggleGroup<String>(
axis: Axis.vertical,
values: {align},
semanticLabel: 'Alignment',
onChanged: (v) => setState(() => align = v.firstOrNull ?? align),
items: const [
ToggleGroupItem('top', Text('Top')),
ToggleGroupItem('middle', Text('Middle')),
ToggleGroupItem('bottom', Text('Bottom')),
],
);
}
API
Read from the source above: each parameter with its type, its default, and what it is for.
ToggleGroupItemclass
One choice in a ToggleGroup.
| Parameter | Type | Default | What it is for |
|---|---|---|---|
valuepositional |
T |
The value it stands for, given back when it is pressed. | |
childpositional |
Widget |
Its label or icon. | |
semanticLabel |
String? |
What a screen reader calls it, for an item that shows an icon alone. | |
enabled |
bool |
true |
False fades it and leaves it out of the group's choices. |
ToggleGroupclass
Toggles joined into one piece. Without multiple, pressing one releases the rest, and pressing the pressed one releases it. The arrow keys move between them.
| Parameter | Type | Default | What it is for |
|---|---|---|---|
valuesrequired |
Set<T> |
The pressed ones. | |
onChangedrequired |
ValueChanged<Set<T>>? |
Null disables the group. | |
itemsrequired |
List<ToggleGroupItem<T>> |
The toggles, in order. | |
multiple |
bool |
false |
Several may be on at once, not one. |
variant |
ToggleVariant |
ToggleVariant.outline |
Plain, or with borders all the time. |
size |
ToggleSize |
ToggleSize.md |
Small, medium or large. |
semanticLabel |
String? |
What the group sets, read before it: 'Text alignment'. | |
spacing |
double |
0 |
The gap between toggles. None joins them into one piece; any sets them apart, each with all its corners round. |
axis |
Axis |
Axis.horizontal |
Vertical stacks them, the up and down arrows moving between them. |
Source
What lands in your app. It is yours from then on: change it as you would your own code, and ashlar add
will not write over it.
lib/ui/toggle_group.dart 157 lines
// Ashlar UI: Toggle Group. Copied into your app by `ashlar add toggle-group`; it is yours to
// change.
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'group.dart';
import 'theme.dart';
import 'toggle.dart';
/// One choice in a [ToggleGroup].
@immutable
class ToggleGroupItem<T> {
const ToggleGroupItem(
this.value,
this.child, {
this.semanticLabel,
this.enabled = true,
});
/// The value it stands for, given back when it is pressed.
final T value;
/// Its label or icon.
final Widget child;
/// What a screen reader calls it, for an item that shows an icon alone.
final String? semanticLabel;
/// False fades it and leaves it out of the group's choices.
final bool enabled;
}
/// Toggles joined into one piece. Without [multiple], pressing one releases the rest, and
/// pressing the pressed one releases it. The arrow keys move between them.
///
/// ```dart
/// ToggleGroup<TextAlign>(
/// values: {align},
/// onChanged: (v) => setState(() => align = v.firstOrNull ?? TextAlign.left),
/// items: [
/// ToggleGroupItem(TextAlign.left, AshlarIcon(AshlarIconsRoundedStroke.textAlignStart), semanticLabel: 'Align left'),
/// ToggleGroupItem(TextAlign.center, AshlarIcon(AshlarIconsRoundedStroke.textAlignCenter), semanticLabel: 'Centre'),
/// ],
/// )
/// ```
class ToggleGroup<T> extends StatelessWidget {
const ToggleGroup({
super.key,
required this.values,
required this.onChanged,
required this.items,
this.multiple = false,
this.variant = ToggleVariant.outline,
this.size = ToggleSize.md,
this.semanticLabel,
this.spacing = 0,
this.axis = Axis.horizontal,
});
/// The pressed ones.
final Set<T> values;
/// Null disables the group.
final ValueChanged<Set<T>>? onChanged;
/// The toggles, in order.
final List<ToggleGroupItem<T>> items;
/// Several may be on at once, not one.
final bool multiple;
/// Plain, or with borders all the time.
final ToggleVariant variant;
/// Small, medium or large.
final ToggleSize size;
/// What the group sets, read before it: 'Text alignment'.
final String? semanticLabel;
/// The gap between toggles. None joins them into one piece; any sets them apart, each
/// with all its corners round.
final double spacing;
/// Vertical stacks them, the up and down arrows moving between them.
final Axis axis;
Widget _layout(List<Widget> toggles) {
if (spacing == 0) {
final joined = JoinedGroup(axis: axis, children: toggles);
return axis == Axis.horizontal
? IntrinsicHeight(child: joined)
: IntrinsicWidth(child: joined);
}
return Flex(
direction: axis,
mainAxisSize: MainAxisSize.min,
children: [
for (final (i, toggle) in toggles.indexed) ...[
if (i > 0)
SizedBox(
width: axis == Axis.horizontal ? spacing : 0,
height: axis == Axis.vertical ? spacing : 0,
),
toggle,
],
],
);
}
void _press(T value, bool on) {
final next = multiple ? {...values} : <T>{};
if (on) {
next.add(value);
} else {
next.remove(value);
}
onChanged?.call(next);
}
@override
Widget build(BuildContext context) => Semantics(
container: true,
label: semanticLabel,
child: Shortcuts(
// Right is next in left-to-right text, and back in right-to-left.
shortcuts: {
SingleActivator(LogicalKeyboardKey.arrowRight): isRtl(context)
? const PreviousFocusIntent()
: const NextFocusIntent(),
const SingleActivator(LogicalKeyboardKey.arrowDown):
const NextFocusIntent(),
SingleActivator(LogicalKeyboardKey.arrowLeft): isRtl(context)
? const NextFocusIntent()
: const PreviousFocusIntent(),
const SingleActivator(LogicalKeyboardKey.arrowUp):
const PreviousFocusIntent(),
},
child: FocusTraversalGroup(
child: _layout([
for (final item in items)
Toggle(
pressed: values.contains(item.value),
onChanged: onChanged == null || !item.enabled
? null
: (on) => _press(item.value, on),
variant: variant,
size: size,
semanticLabel: item.semanticLabel,
child: item.child,
),
]),
),
),
);
}