wonders/lib/ui/common/controls/buttons.dart

244 lines
7.4 KiB
Dart
Raw Normal View History

2022-08-29 20:38:28 -06:00
import 'package:wonders/common_libs.dart';
import 'package:wonders/ui/common/app_icons.dart';
2022-08-29 20:38:28 -06:00
/// Shared methods across button types
Widget _buildIcon(BuildContext context, AppIcons icon, {required bool isSecondary, required double? size}) =>
AppIcon(icon, color: isSecondary ? $styles.colors.black : $styles.colors.offWhite, size: size ?? 18);
2022-08-29 20:38:28 -06:00
/// The core button that drives all other buttons.
class AppBtn extends StatelessWidget {
// ignore: prefer_const_constructors_in_immutables
AppBtn({
super.key,
2022-08-29 20:38:28 -06:00
required this.onPressed,
required this.semanticLabel,
this.enableFeedback = true,
this.pressEffect = true,
this.child,
this.padding,
this.expand = false,
this.isSecondary = false,
this.circular = false,
this.minimumSize,
this.bgColor,
this.border,
this.focusNode,
this.onFocusChanged,
}) : _builder = null;
2022-08-29 20:38:28 -06:00
AppBtn.from({
super.key,
2022-08-29 20:38:28 -06:00
required this.onPressed,
this.enableFeedback = true,
this.pressEffect = true,
this.padding,
this.expand = false,
this.isSecondary = false,
this.minimumSize,
this.bgColor,
this.border,
this.focusNode,
this.onFocusChanged,
2022-08-29 20:38:28 -06:00
String? semanticLabel,
String? text,
AppIcons? icon,
2022-08-29 20:38:28 -06:00
double? iconSize,
}) : child = null,
circular = false {
if (semanticLabel == null && text == null) {
throw ('AppBtn.from must include either text or semanticLabel');
}
2022-08-29 20:38:28 -06:00
this.semanticLabel = semanticLabel ?? text ?? '';
_builder = (context) {
if (text == null && icon == null) return SizedBox.shrink();
Text? txt = text == null
? null
: Text(text.toUpperCase(),
style: $styles.text.btn, textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: false));
Widget? icn = icon == null ? null : _buildIcon(context, icon, isSecondary: isSecondary, size: iconSize);
if (txt != null && icn != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [txt, Gap($styles.insets.xs), icn],
);
} else {
return (txt ?? icn)!;
}
};
}
// ignore: prefer_const_constructors_in_immutables
AppBtn.basic({
super.key,
2022-08-29 20:38:28 -06:00
required this.onPressed,
required this.semanticLabel,
this.enableFeedback = true,
this.pressEffect = true,
this.child,
this.padding = EdgeInsets.zero,
this.isSecondary = false,
this.circular = false,
this.minimumSize,
this.focusNode,
this.onFocusChanged,
2022-08-29 20:38:28 -06:00
}) : expand = false,
bgColor = Colors.transparent,
border = null,
_builder = null;
2022-08-29 20:38:28 -06:00
// interaction:
2023-01-03 10:35:53 -07:00
final VoidCallback? onPressed;
2022-08-29 20:38:28 -06:00
late final String semanticLabel;
final bool enableFeedback;
final FocusNode? focusNode;
final void Function(bool hasFocus)? onFocusChanged;
2022-08-29 20:38:28 -06:00
// content:
late final Widget? child;
late final WidgetBuilder? _builder;
// layout:
final EdgeInsets? padding;
final bool expand;
final bool circular;
final Size? minimumSize;
// style:
final bool isSecondary;
final BorderSide? border;
final Color? bgColor;
final bool pressEffect;
@override
Widget build(BuildContext context) {
Color defaultColor = isSecondary ? $styles.colors.white : $styles.colors.greyStrong;
Color textColor = isSecondary ? $styles.colors.black : $styles.colors.white;
BorderSide side = border ?? BorderSide.none;
Widget content = _builder?.call(context) ?? child ?? SizedBox.shrink();
if (expand) content = Center(child: content);
OutlinedBorder shape = circular
? CircleBorder(side: side)
: RoundedRectangleBorder(side: side, borderRadius: BorderRadius.circular($styles.corners.md));
ButtonStyle style = ButtonStyle(
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize ?? Size.zero),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
splashFactory: NoSplash.splashFactory,
backgroundColor: ButtonStyleButton.allOrNull<Color>(bgColor ?? defaultColor),
overlayColor: ButtonStyleButton.allOrNull<Color>(Colors.transparent), // disable default press effect
2022-08-29 20:38:28 -06:00
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding ?? EdgeInsets.all($styles.insets.md)),
2023-02-28 10:06:46 -07:00
2022-08-29 20:38:28 -06:00
enableFeedback: enableFeedback,
);
Widget button = _CustomFocusBuilder(
focusNode: focusNode,
onFocusChanged: onFocusChanged,
builder: (context, focus) => Stack(
children: [
Opacity(
opacity: onPressed == null ? 0.5 : 1.0,
child: TextButton(
onPressed: onPressed,
style: style,
focusNode: focus,
child: DefaultTextStyle(
style: DefaultTextStyle.of(context).style.copyWith(color: textColor),
child: content,
),
),
),
if (focus.hasFocus)
Positioned.fill(
child: IgnorePointer(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular($styles.corners.md),
border: Border.all(color: $styles.colors.accent1, width: 3))),
),
)
],
2022-08-29 20:38:28 -06:00
),
);
// add press effect:
if (pressEffect && onPressed != null) button = _ButtonPressEffect(button);
2022-08-29 20:38:28 -06:00
// add semantics?
if (semanticLabel.isEmpty) return button;
return Semantics(
label: semanticLabel,
button: true,
container: true,
child: ExcludeSemantics(child: button),
);
2022-08-29 20:38:28 -06:00
}
}
/// //////////////////////////////////////////////////
/// _ButtonDecorator
/// Add a transparency-based press effect to buttons.
/// //////////////////////////////////////////////////
class _ButtonPressEffect extends StatefulWidget {
const _ButtonPressEffect(this.child);
2022-08-29 20:38:28 -06:00
final Widget child;
@override
State<_ButtonPressEffect> createState() => _ButtonPressEffectState();
}
class _ButtonPressEffectState extends State<_ButtonPressEffect> {
bool _isDown = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
excludeFromSemantics: true,
onTapDown: (_) => setState(() => _isDown = true),
onTapUp: (_) => setState(() => _isDown = false), // not called, TextButton swallows this.
onTapCancel: () => setState(() => _isDown = false),
behavior: HitTestBehavior.translucent,
child: Opacity(
opacity: _isDown ? 0.7 : 1,
child: ExcludeSemantics(child: widget.child),
),
);
}
}
class _CustomFocusBuilder extends StatefulWidget {
const _CustomFocusBuilder({required this.builder, this.focusNode, this.onFocusChanged});
final Widget Function(BuildContext context, FocusNode focus) builder;
final void Function(bool hasFocus)? onFocusChanged;
final FocusNode? focusNode;
@override
State<_CustomFocusBuilder> createState() => _CustomFocusBuilderState();
}
class _CustomFocusBuilderState extends State<_CustomFocusBuilder> {
late final FocusNode _focusNode;
@override
void initState() {
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.addListener(_handleFocusChanged);
super.initState();
}
void _handleFocusChanged() {
widget.onFocusChanged?.call(_focusNode.hasFocus);
if (mounted) {
setState(() {});
}
}
@override
Widget build(BuildContext context) {
return widget.builder.call(context, _focusNode);
}
}