Toggle
A button that stays pressed: bold, italic, a filter.
import 'package:ashlar_icons/ashlar_icons.dart';
import 'package:flutter/widgets.dart';
import 'ui/toggle.dart';
class ToggleDemo extends StatefulWidget {
const ToggleDemo({super.key});
@override
State<ToggleDemo> createState() => _ToggleDemoState();
}
class _ToggleDemoState extends State<ToggleDemo> {
bool bold = true, starred = false;
@override
Widget build(BuildContext context) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Toggle(
pressed: bold,
onChanged: (v) => setState(() => bold = v),
semanticLabel: 'Bold',
child: const AshlarIcon(AshlarIconsRoundedStroke.bold),
),
const SizedBox(width: 8),
Toggle(
pressed: starred,
onChanged: (v) => setState(() => starred = v),
variant: ToggleVariant.outline,
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
AshlarIcon(AshlarIconsRoundedStroke.star),
SizedBox(width: 6),
Text('Starred'),
],
),
),
],
);
}
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
It writes theme.dart, palette.dart, group.dart, pressable.dart and toggle.dart into lib/ui, keeping any you already have. It needs pressable and group, which come with it.
Examples
Other ways to use it, each running live, with its code.
Outlined
A border all the time.
// Outlined: A border all the time.
import 'package:flutter/widgets.dart';
import 'ui/toggle.dart';
class ToggleOutlineExample extends StatefulWidget {
const ToggleOutlineExample({super.key});
@override
State<ToggleOutlineExample> createState() => _ToggleOutlineExampleState();
}
class _ToggleOutlineExampleState extends State<ToggleOutlineExample> {
var on = false;
@override
Widget build(BuildContext context) => Toggle(
variant: ToggleVariant.outline,
pressed: on,
onChanged: (v) => setState(() => on = v),
child: const Text('Show hidden files'),
);
}
Sizes
Small, medium and large.
// Sizes: Small, medium and large.
import 'package:ashlar_icons/ashlar_icons.dart';
import 'package:flutter/widgets.dart';
import 'ui/toggle.dart';
class ToggleSizesExample extends StatefulWidget {
const ToggleSizesExample({super.key});
@override
State<ToggleSizesExample> createState() => _ToggleSizesExampleState();
}
class _ToggleSizesExampleState extends State<ToggleSizesExample> {
final on = <ToggleSize>{ToggleSize.md};
@override
Widget build(BuildContext context) => Row(
mainAxisSize: MainAxisSize.min,
children: [
for (final size in ToggleSize.values) ...[
Toggle(
size: size,
pressed: on.contains(size),
semanticLabel: 'Bold',
onChanged: (v) => setState(() => v ? on.add(size) : on.remove(size)),
child: const AshlarIcon(AshlarIconsRoundedStroke.bold),
),
const SizedBox(width: 8),
],
],
);
}
Disabled
Null onChanged.
// Disabled: Null onChanged.
import 'package:flutter/widgets.dart';
import 'ui/toggle.dart';
class ToggleDisabledExample extends StatelessWidget {
const ToggleDisabledExample({super.key});
@override
Widget build(BuildContext context) => const Toggle(pressed: true, onChanged: null, child: Text('Locked'));
}
API
Read from the source above: each parameter with its type, its default, and what it is for.
ToggleVariantenum
| Value | What it is for |
|---|---|
plain | No border until it is on. |
outline | A border all the time. |
ToggleSizeenum
| Value | What it is for |
|---|---|
sm | |
md | |
lg |
Toggleclass
A button that stays pressed: bold, italic, a filter. Give an icon-only toggle a semanticLabel.
| Parameter | Type | Default | What it is for |
|---|---|---|---|
pressedrequired |
bool |
Whether it is on. | |
onChangedrequired |
ValueChanged<bool>? |
Null disables it. | |
childrequired |
Widget |
Its label or icon. | |
variant |
ToggleVariant |
ToggleVariant.plain |
Plain, or with a border all the time. |
size |
ToggleSize |
ToggleSize.md |
Small, medium or large. |
semanticLabel |
String? |
What a screen reader calls it, for a toggle that shows an icon alone. |
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.dart 114 lines
// Ashlar UI: Toggle. Copied into your app by `ashlar add toggle`; it is yours to change.
import 'package:flutter/widgets.dart';
import 'group.dart';
import 'pressable.dart';
import 'theme.dart';
enum ToggleVariant {
/// No border until it is on.
plain,
/// A border all the time.
outline,
}
enum ToggleSize { sm, md, lg }
/// A button that stays pressed: bold, italic, a filter. Give an icon-only toggle a
/// [semanticLabel].
///
/// ```dart
/// Toggle(pressed: bold, onChanged: (v) => setState(() => bold = v), semanticLabel: 'Bold', child: AshlarIcon(AshlarIconsRoundedStroke.bold))
/// ```
class Toggle extends StatelessWidget {
const Toggle({
super.key,
required this.pressed,
required this.onChanged,
required this.child,
this.variant = ToggleVariant.plain,
this.size = ToggleSize.md,
this.semanticLabel,
});
/// Whether it is on.
final bool pressed;
/// Null disables it.
final ValueChanged<bool>? onChanged;
/// Its label or icon.
final Widget child;
/// Plain, or with a border all the time.
final ToggleVariant variant;
/// Small, medium or large.
final ToggleSize size;
/// What a screen reader calls it, for a toggle that shows an icon alone.
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final t = AshlarTheme.of(context);
final c = t.colors;
final height = t.control(switch (size) {
ToggleSize.sm => 32.0,
ToggleSize.md => 36.0,
ToggleSize.lg => 40.0,
});
return Pressable(
onPressed: onChanged == null ? null : () => onChanged!(!pressed),
toggled: pressed,
semanticLabel: semanticLabel,
builder: (context, state) {
final fg = pressed
? c.accentForeground
: (state.hovered ? c.mutedForeground : c.foreground);
return Opacity(
opacity: state.enabled ? 1 : .5,
child: AnimatedContainer(
duration: t.duration,
// Taller with larger text rather than cutting it off, and never stretched.
height: grownHeight(context, height),
constraints: BoxConstraints(minWidth: height),
padding: EdgeInsets.symmetric(
horizontal: size == ToggleSize.sm ? 6 : 8,
),
decoration: BoxDecoration(
color: pressed
? c.accent
: (state.hovered ? c.muted : c.muted.withValues(alpha: 0)),
borderRadius: joinedRadius(context, t.radiusMd),
border: variant == ToggleVariant.outline
? joinedBorder(context, state.focused ? c.ring : c.input)
: null,
boxShadow: [
if (state.focused) ...t.focusRing,
if (variant == ToggleVariant.outline) ...t.shadowXs,
],
),
// Centred, and as wide as its label unless a group stretches it.
child: Align(
widthFactor: 1,
child: DefaultTextStyle.merge(
style: t.text.sm.copyWith(
color: fg,
fontWeight: FontWeight.w500,
),
maxLines: 1,
child: IconTheme.merge(
data: IconThemeData(color: fg, size: 16),
child: child,
),
),
),
),
);
},
);
}
}