import 'package:wonders/common_libs.dart'; import 'package:wonders/ui/common/app_icons.dart'; /// 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); /// The core button that drives all other buttons. class AppBtn extends StatelessWidget { // ignore: prefer_const_constructors_in_immutables AppBtn({ super.key, 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; AppBtn.from({ super.key, 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, String? semanticLabel, String? text, AppIcons? icon, double? iconSize, }) : child = null, circular = false { if (semanticLabel == null && text == null) { throw ('AppBtn.from must include either text or semanticLabel'); } 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, 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, }) : expand = false, bgColor = Colors.transparent, border = null, _builder = null; // interaction: final VoidCallback? onPressed; late final String semanticLabel; final bool enableFeedback; final FocusNode? focusNode; final void Function(bool hasFocus)? onFocusChanged; // 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(minimumSize ?? Size.zero), tapTargetSize: MaterialTapTargetSize.shrinkWrap, splashFactory: NoSplash.splashFactory, backgroundColor: ButtonStyleButton.allOrNull(bgColor ?? defaultColor), overlayColor: ButtonStyleButton.allOrNull(Colors.transparent), // disable default press effect shape: ButtonStyleButton.allOrNull(shape), padding: ButtonStyleButton.allOrNull(padding ?? EdgeInsets.all($styles.insets.md)), 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))), ), ) ], ), ); // add press effect: if (pressEffect && onPressed != null) button = _ButtonPressEffect(button); // add semantics? if (semanticLabel.isEmpty) return button; return Semantics( label: semanticLabel, button: true, container: true, child: ExcludeSemantics(child: button), ); } } /// ////////////////////////////////////////////////// /// _ButtonDecorator /// Add a transparency-based press effect to buttons. /// ////////////////////////////////////////////////// class _ButtonPressEffect extends StatefulWidget { const _ButtonPressEffect(this.child); 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); } }