| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- import 'package:font_awesome_flutter/font_awesome_flutter.dart';
- import 'package:flutter/material.dart';
- import 'package:auto_size_text/auto_size_text.dart';
- class FFButtonOptions {
- const FFButtonOptions({
- this.textAlign,
- this.textStyle,
- this.elevation,
- this.height,
- this.width,
- this.padding,
- this.color,
- this.disabledColor,
- this.disabledTextColor,
- this.splashColor,
- this.iconSize,
- this.iconColor,
- this.iconAlignment,
- this.iconPadding,
- this.borderRadius,
- this.borderSide,
- this.hoverColor,
- this.hoverBorderSide,
- this.hoverTextColor,
- this.hoverElevation,
- this.maxLines,
- });
- final TextAlign? textAlign;
- final TextStyle? textStyle;
- final double? elevation;
- final double? height;
- final double? width;
- final EdgeInsetsGeometry? padding;
- final Color? color;
- final Color? disabledColor;
- final Color? disabledTextColor;
- final int? maxLines;
- final Color? splashColor;
- final double? iconSize;
- final Color? iconColor;
- final IconAlignment? iconAlignment;
- final EdgeInsetsGeometry? iconPadding;
- final BorderRadius? borderRadius;
- final BorderSide? borderSide;
- final Color? hoverColor;
- final BorderSide? hoverBorderSide;
- final Color? hoverTextColor;
- final double? hoverElevation;
- }
- class FFButtonWidget extends StatefulWidget {
- const FFButtonWidget({
- super.key,
- required this.text,
- required this.onPressed,
- this.icon,
- this.iconData,
- required this.options,
- this.showLoadingIndicator = true,
- this.focusNode,
- });
- final String text;
- final Widget? icon;
- final IconData? iconData;
- final Function()? onPressed;
- final FFButtonOptions options;
- final bool showLoadingIndicator;
- final FocusNode? focusNode;
- @override
- State<FFButtonWidget> createState() => _FFButtonWidgetState();
- }
- class _FFButtonWidgetState extends State<FFButtonWidget> {
- bool loading = false;
- late FocusNode _internalFocusNode;
- FocusNode get _focusNode => widget.focusNode ?? _internalFocusNode;
- int get maxLines => widget.options.maxLines ?? 1;
- String? get text =>
- widget.options.textStyle?.fontSize == 0 ? null : widget.text;
- @override
- void initState() {
- super.initState();
- _internalFocusNode = FocusNode();
- }
- @override
- void dispose() {
- _internalFocusNode.dispose();
- super.dispose();
- }
- @override
- Widget build(BuildContext context) {
- Widget textWidget = loading
- ? SizedBox(
- width: widget.options.width == null
- ? _getTextWidth(text, widget.options.textStyle, maxLines)
- : null,
- child: Center(
- child: SizedBox(
- width: 23,
- height: 23,
- child: CircularProgressIndicator(
- valueColor: AlwaysStoppedAnimation<Color>(
- widget.options.textStyle?.color ?? Colors.white,
- ),
- ),
- ),
- ),
- )
- : AutoSizeText(
- text ?? '',
- style:
- text == null ? null : widget.options.textStyle?.withoutColor(),
- textAlign: widget.options.textAlign,
- maxLines: maxLines,
- overflow: TextOverflow.ellipsis,
- );
- final onPressed = widget.onPressed != null
- ? (widget.showLoadingIndicator
- ? () async {
- if (loading) {
- return;
- }
- setState(() => loading = true);
- try {
- await widget.onPressed!();
- } finally {
- if (mounted) {
- setState(() => loading = false);
- }
- }
- }
- : () => widget.onPressed!())
- : null;
- ButtonStyle style = ButtonStyle(
- shape: WidgetStateProperty.resolveWith<OutlinedBorder>((states) {
- if (states.contains(WidgetState.hovered) &&
- widget.options.hoverBorderSide != null) {
- return RoundedRectangleBorder(
- borderRadius:
- widget.options.borderRadius ?? BorderRadius.circular(8),
- side: widget.options.hoverBorderSide!,
- );
- }
- return RoundedRectangleBorder(
- borderRadius: widget.options.borderRadius ?? BorderRadius.circular(8),
- side: widget.options.borderSide ?? BorderSide.none,
- );
- }),
- foregroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
- if (states.contains(WidgetState.disabled) &&
- widget.options.disabledTextColor != null) {
- return widget.options.disabledTextColor;
- }
- if (states.contains(WidgetState.hovered) &&
- widget.options.hoverTextColor != null) {
- return widget.options.hoverTextColor;
- }
- return widget.options.textStyle?.color ?? Colors.white;
- }),
- backgroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
- if (states.contains(WidgetState.disabled) &&
- widget.options.disabledColor != null) {
- return widget.options.disabledColor;
- }
- if (states.contains(WidgetState.hovered) &&
- widget.options.hoverColor != null) {
- return widget.options.hoverColor;
- }
- return widget.options.color;
- }),
- overlayColor: WidgetStateProperty.resolveWith<Color?>((states) {
- if (states.contains(WidgetState.pressed)) {
- return widget.options.splashColor;
- }
- return widget.options.hoverColor == null ? null : Colors.transparent;
- }),
- padding: WidgetStateProperty.all(
- widget.options.padding ??
- const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
- ),
- elevation: WidgetStateProperty.resolveWith<double?>((states) {
- if (states.contains(WidgetState.hovered) &&
- widget.options.hoverElevation != null) {
- return widget.options.hoverElevation!;
- }
- return widget.options.elevation ?? 2.0;
- }),
- iconColor: WidgetStateProperty.resolveWith<Color?>((states) {
- if (states.contains(WidgetState.disabled) &&
- widget.options.disabledTextColor != null) {
- return widget.options.disabledTextColor;
- }
- if (states.contains(WidgetState.hovered) &&
- widget.options.hoverTextColor != null) {
- return widget.options.hoverTextColor;
- }
- return widget.options.iconColor;
- }),
- );
- if ((widget.icon != null || widget.iconData != null) && !loading) {
- Widget icon = widget.icon ??
- FaIcon(
- widget.iconData!,
- size: widget.options.iconSize,
- color: widget.options.iconColor,
- );
- if (text == null) {
- return Container(
- height: widget.options.height,
- width: widget.options.width,
- decoration: BoxDecoration(
- border: Border.fromBorderSide(
- widget.options.borderSide ?? BorderSide.none,
- ),
- borderRadius:
- widget.options.borderRadius ?? BorderRadius.circular(8),
- ),
- child: IconButton(
- splashRadius: 1.0,
- icon: Padding(
- padding: widget.options.iconPadding ?? EdgeInsets.zero,
- child: icon,
- ),
- onPressed: onPressed,
- style: style,
- focusNode: _focusNode,
- ),
- );
- }
- return SizedBox(
- height: widget.options.height,
- width: widget.options.width,
- child: ElevatedButton.icon(
- icon: Padding(
- padding: widget.options.iconPadding ?? EdgeInsets.zero,
- child: icon,
- ),
- label: textWidget,
- onPressed: onPressed,
- style: style,
- iconAlignment: widget.options.iconAlignment ?? IconAlignment.start,
- focusNode: _focusNode,
- ),
- );
- }
- return SizedBox(
- height: widget.options.height,
- width: widget.options.width,
- child: ElevatedButton(
- onPressed: onPressed,
- style: style,
- focusNode: _focusNode,
- child: textWidget,
- ),
- );
- }
- }
- extension _WithoutColorExtension on TextStyle {
- TextStyle withoutColor() => TextStyle(
- inherit: inherit,
- color: null,
- backgroundColor: backgroundColor,
- fontSize: fontSize,
- fontWeight: fontWeight,
- fontStyle: fontStyle,
- letterSpacing: letterSpacing,
- wordSpacing: wordSpacing,
- textBaseline: textBaseline,
- height: height,
- leadingDistribution: leadingDistribution,
- locale: locale,
- foreground: foreground,
- background: background,
- shadows: shadows,
- fontFeatures: fontFeatures,
- decoration: decoration,
- decorationColor: decorationColor,
- decorationStyle: decorationStyle,
- decorationThickness: decorationThickness,
- debugLabel: debugLabel,
- fontFamily: fontFamily,
- fontFamilyFallback: fontFamilyFallback,
- // The _package field is private so unfortunately we can't set it here,
- // but it's almost always unset anyway.
- // package: _package,
- overflow: overflow,
- );
- }
- // Slightly hacky method of getting the layout width of the provided text.
- double? _getTextWidth(String? text, TextStyle? style, int maxLines) =>
- text != null
- ? (TextPainter(
- text: TextSpan(text: text, style: style),
- textDirection: TextDirection.ltr,
- maxLines: maxLines,
- )..layout())
- .size
- .width
- : null;
- class FFFocusIndicator extends StatefulWidget {
- final Widget Function(FocusNode focusNode)? builder;
- final Widget? child;
- final Border? border;
- final BorderRadius? borderRadius;
- final EdgeInsetsGeometry? padding;
- final void Function()? onTap;
- final void Function()? onLongPress;
- final void Function()? onDoubleTap;
- const FFFocusIndicator({
- super.key,
- this.builder,
- this.child,
- this.border,
- this.borderRadius,
- this.padding,
- this.onTap,
- this.onLongPress,
- this.onDoubleTap,
- }) : assert(
- builder != null || child != null,
- 'Either builder or child must be provided',
- );
- @override
- State<FFFocusIndicator> createState() => _FFFocusIndicatorState();
- }
- class _FFFocusIndicatorState extends State<FFFocusIndicator> {
- late FocusNode _focusNode;
- bool _hasFocus = false;
- @override
- void initState() {
- super.initState();
- _focusNode = FocusNode();
- _focusNode.addListener(_onFocusChange);
- }
- @override
- void dispose() {
- _focusNode.removeListener(_onFocusChange);
- _focusNode.dispose();
- super.dispose();
- }
- void _onFocusChange() {
- if (mounted) {
- if (_focusNode.hasFocus) {
- // No single ScrollPositionAlignmentPolicy scrolls in both directions.
- // keepVisibleAtEnd scrolls DOWN (handles Shift+Tab wrap first → last).
- // keepVisibleAtStart scrolls UP (handles Tab wrap last → first).
- // Each is a no-op when the widget is already visible. We call
- // keepVisibleAtEnd first so that keepVisibleAtStart gets the final
- // say — ensuring the top of the widget is shown when it's taller
- // than the viewport.
- WidgetsBinding.instance.addPostFrameCallback((_) {
- if (mounted && _focusNode.hasFocus) {
- Scrollable.ensureVisible(
- context,
- alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
- );
- Scrollable.ensureVisible(
- context,
- alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
- );
- }
- });
- }
- setState(() {
- _hasFocus = _focusNode.hasFocus;
- });
- }
- }
- @override
- Widget build(BuildContext context) {
- final bool hasInteractions = widget.onTap != null ||
- widget.onLongPress != null ||
- widget.onDoubleTap != null;
- Widget childWidget;
- if (widget.builder != null) {
- // Builder mode: pass focus node to builder
- childWidget = widget.builder!(_focusNode);
- } else if (hasInteractions) {
- // Child mode with interactions: wrap in InkWell
- childWidget = InkWell(
- splashColor: Colors.transparent,
- hoverColor: Colors.transparent,
- highlightColor: Colors.transparent,
- focusColor: Colors.transparent,
- focusNode: _focusNode,
- onTap: widget.onTap,
- onLongPress: widget.onLongPress,
- onDoubleTap: widget.onDoubleTap,
- child: widget.child!,
- );
- } else {
- // Child mode without interactions: just use child
- childWidget = widget.child!;
- }
- return AnimatedContainer(
- duration: const Duration(milliseconds: 200),
- padding: _hasFocus ? widget.padding : null,
- decoration: BoxDecoration(
- border: _hasFocus ? widget.border : null,
- borderRadius: widget.borderRadius ?? BorderRadius.circular(4),
- ),
- child: childWidget,
- );
- }
- }
|