2022-08-29 20:38:28 -06:00
|
|
|
import 'package:wonders/common_libs.dart';
|
2022-11-22 10:56:53 -07:00
|
|
|
import 'package:wonders/ui/common/app_icons.dart';
|
2022-08-29 20:38:28 -06:00
|
|
|
|
|
|
|
/// Shared methods across button types
|
2022-11-22 10:56:53 -07:00
|
|
|
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({
|
2024-02-20 13:56:39 -08:00
|
|
|
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,
|
2023-11-29 15:59:25 -07:00
|
|
|
this.focusNode,
|
|
|
|
this.onFocusChanged,
|
2024-02-20 13:56:39 -08:00
|
|
|
}) : _builder = null;
|
2022-08-29 20:38:28 -06:00
|
|
|
|
|
|
|
AppBtn.from({
|
2024-02-20 13:56:39 -08:00
|
|
|
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,
|
2023-11-29 15:59:25 -07:00
|
|
|
this.focusNode,
|
|
|
|
this.onFocusChanged,
|
2022-08-29 20:38:28 -06:00
|
|
|
String? semanticLabel,
|
|
|
|
String? text,
|
2022-11-22 10:56:53 -07:00
|
|
|
AppIcons? icon,
|
2022-08-29 20:38:28 -06:00
|
|
|
double? iconSize,
|
|
|
|
}) : child = null,
|
2024-02-20 13:56:39 -08:00
|
|
|
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({
|
2024-02-20 13:56:39 -08:00
|
|
|
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,
|
2023-11-29 15:59:25 -07:00
|
|
|
this.focusNode,
|
|
|
|
this.onFocusChanged,
|
2022-08-29 20:38:28 -06:00
|
|
|
}) : expand = false,
|
|
|
|
bgColor = Colors.transparent,
|
|
|
|
border = null,
|
2024-02-20 13:56:39 -08:00
|
|
|
_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;
|
2023-11-29 15:59:25 -07:00
|
|
|
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),
|
2023-03-01 14:36:30 -07:00
|
|
|
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,
|
|
|
|
);
|
|
|
|
|
2023-03-01 14:36:30 -07:00
|
|
|
Widget button = _CustomFocusBuilder(
|
2023-11-29 15:59:25 -07:00
|
|
|
focusNode: focusNode,
|
|
|
|
onFocusChanged: onFocusChanged,
|
2023-03-01 14:36:30 -07:00
|
|
|
builder: (context, focus) => Stack(
|
|
|
|
children: [
|
2023-11-29 15:59:25 -07:00
|
|
|
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,
|
|
|
|
),
|
2023-03-01 14:36:30 -07:00
|
|
|
),
|
|
|
|
),
|
|
|
|
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:
|
2023-11-29 15:59:25 -07:00
|
|
|
if (pressEffect && onPressed != null) button = _ButtonPressEffect(button);
|
2022-08-29 20:38:28 -06:00
|
|
|
|
2022-09-19 14:21:30 -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 {
|
2024-02-20 13:56:39 -08:00
|
|
|
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),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2023-03-01 14:36:30 -07:00
|
|
|
|
|
|
|
class _CustomFocusBuilder extends StatefulWidget {
|
2024-02-20 13:56:39 -08:00
|
|
|
const _CustomFocusBuilder({required this.builder, this.focusNode, this.onFocusChanged});
|
2023-03-01 14:36:30 -07:00
|
|
|
final Widget Function(BuildContext context, FocusNode focus) builder;
|
2023-11-29 15:59:25 -07:00
|
|
|
final void Function(bool hasFocus)? onFocusChanged;
|
|
|
|
final FocusNode? focusNode;
|
2023-03-01 14:36:30 -07:00
|
|
|
@override
|
|
|
|
State<_CustomFocusBuilder> createState() => _CustomFocusBuilderState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _CustomFocusBuilderState extends State<_CustomFocusBuilder> {
|
2023-11-29 15:59:25 -07:00
|
|
|
late final FocusNode _focusNode;
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
_focusNode = widget.focusNode ?? FocusNode();
|
|
|
|
_focusNode.addListener(_handleFocusChanged);
|
|
|
|
super.initState();
|
|
|
|
}
|
|
|
|
|
|
|
|
void _handleFocusChanged() {
|
|
|
|
widget.onFocusChanged?.call(_focusNode.hasFocus);
|
2024-01-22 15:35:19 -07:00
|
|
|
if (mounted) {
|
|
|
|
setState(() {});
|
|
|
|
}
|
2023-11-29 15:59:25 -07:00
|
|
|
}
|
2023-03-01 14:36:30 -07:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return widget.builder.call(context, _focusNode);
|
|
|
|
}
|
|
|
|
}
|