| 1 | import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; |
| 2 | import 'package:flutter/material.dart'; |
| 3 | import 'package:flutter_svg/flutter_svg.dart'; |
| 4 | |
| 5 | class ModernButton extends StatelessWidget { |
| 6 | final double size; |
| 7 | final String? svgPath; |
| 8 | final Widget? icon; |
| 9 | final VoidCallback onPressed; |
| 10 | final Color? iconColor; |
| 11 | final Color? backgroundColor; |
| 12 | final double? iconSize; |
| 13 | final String? label; |
| 14 | |
| 15 | /// Accessible name announced by screen readers. Falls back to [label] (the |
| 16 | /// visible caption) when omitted, so one of the two is required. Must be |
| 17 | /// localized by the caller. |
| 18 | final String? semanticLabel; |
| 19 | static const iconSvgSizeRatio = 2 / 3; |
| 20 | |
| 21 | const ModernButton( |
| 22 | {super.key, |
| 23 | required this.size, |
| 24 | required this.icon, |
| 25 | required this.onPressed, |
| 26 | this.iconSize, |
| 27 | this.iconColor, |
| 28 | this.backgroundColor, |
| 29 | this.label, |
| 30 | this.semanticLabel}) |
| 31 | : svgPath = null; |
| 32 | |
| 33 | const ModernButton.svg( |
| 34 | {super.key, |
| 35 | required this.size, |
| 36 | required this.svgPath, |
| 37 | required this.onPressed, |
| 38 | this.iconSize, |
| 39 | this.iconColor, |
| 40 | this.backgroundColor, |
| 41 | this.label, |
| 42 | this.semanticLabel}) |
| 43 | : icon = null; |
| 44 | |
| 45 | @override |
| 46 | Widget build(BuildContext context) { |
| 47 | final resolvedIconColor = iconColor ?? Theme.of(context).colorScheme.primary; |
| 48 | final resolvedIconSize = iconSize ?? size * (svgPath != null ? 1 : iconSvgSizeRatio); |
| 49 | final resolvedBackgroundColor = |
| 50 | backgroundColor ?? Theme.of(context).colorScheme.surfaceContainer; |
| 51 | final Widget resolvedIcon = svgPath != null |
| 52 | ? CakeImageWidget( |
| 53 | imageUrl: svgPath!, |
| 54 | width: resolvedIconSize, |
| 55 | height: resolvedIconSize, |
| 56 | fit: BoxFit.contain, |
| 57 | alignment: Alignment.center, |
| 58 | allowDrawingOutsideViewBox: true, |
| 59 | colorFilter: ColorFilter.mode(resolvedIconColor, BlendMode.srcIn), |
| 60 | ) |
| 61 | : IconTheme( |
| 62 | data: IconThemeData(color: resolvedIconColor, size: resolvedIconSize), |
| 63 | child: icon!, |
| 64 | ); |
| 65 | |
| 66 | // A single accessibility node for the whole control: the visible caption |
| 67 | // duplicates the accessible name, so it is excluded to avoid a second, |
| 68 | // detached stop for screen readers. |
| 69 | return MergeSemantics( |
| 70 | child: Semantics( |
| 71 | button: true, |
| 72 | label: semanticLabel ?? label, |
| 73 | child: Column( |
| 74 | spacing: 8, |
| 75 | children: [ |
| 76 | Container( |
| 77 | decoration: BoxDecoration( |
| 78 | color: resolvedBackgroundColor, |
| 79 | borderRadius: BorderRadius.circular(size), |
| 80 | ), |
| 81 | width: size, |
| 82 | height: size, |
| 83 | child: IconButton( |
| 84 | padding: EdgeInsets.zero, |
| 85 | onPressed: onPressed, |
| 86 | icon: ExcludeSemantics(child: resolvedIcon), |
| 87 | ), |
| 88 | ), |
| 89 | if (label != null && label!.isNotEmpty) |
| 90 | ExcludeSemantics( |
| 91 | child: Text( |
| 92 | label!, |
| 93 | style: TextStyle( |
| 94 | fontSize: 12, |
| 95 | fontWeight: FontWeight.w400, |
| 96 | color: Theme.of(context).colorScheme.onSurface), |
| 97 | ), |
| 98 | ) |
| 99 | ], |
| 100 | ), |
| 101 | ), |
| 102 | ); |
| 103 | } |
| 104 | } |