import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/typography.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class NumberTextField extends StatefulWidget { final TextEditingController? controller; final FocusNode? focusNode; final int min; final int max; final int step; final double arrowsWidth; final double arrowsHeight; final EdgeInsets contentPadding; final double borderWidth; final ValueChanged? onChanged; const NumberTextField({ Key? key, this.controller, this.focusNode, this.min = 0, this.max = 999, this.step = 1, this.arrowsWidth = 24, this.arrowsHeight = kMinInteractiveDimension, this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), this.borderWidth = 2, this.onChanged, }) : super(key: key); @override State createState() => _NumberTextFieldState(); } class _NumberTextFieldState extends State { late TextEditingController _controller; late FocusNode _focusNode; bool _canGoUp = false; bool _canGoDown = false; @override void initState() { super.initState(); _controller = widget.controller ?? TextEditingController(); _focusNode = widget.focusNode ?? FocusNode(); _updateArrows(int.tryParse(_controller.text)); } @override void didUpdateWidget(covariant NumberTextField oldWidget) { super.didUpdateWidget(oldWidget); _controller = widget.controller ?? _controller; _focusNode = widget.focusNode ?? _focusNode; _updateArrows(int.tryParse(_controller.text)); } @override Widget build(BuildContext context) => TextField( style: textMediumSemiBold(color: Theme.of(context).extension()!.titleColor), enableInteractiveSelection: false, textAlign: TextAlign.center, textAlignVertical: TextAlignVertical.bottom, controller: _controller, focusNode: _focusNode, textInputAction: TextInputAction.done, keyboardType: TextInputType.number, maxLength: widget.max.toString().length + (widget.min.isNegative ? 1 : 0), decoration: InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.all(0), fillColor: Colors.transparent, counterText: '', isDense: true, filled: true, suffixIconConstraints: BoxConstraints( maxHeight: widget.arrowsHeight, maxWidth: widget.arrowsWidth + widget.contentPadding.right), prefixIconConstraints: BoxConstraints( maxHeight: widget.arrowsHeight, maxWidth: widget.arrowsWidth + widget.contentPadding.left), prefixIcon: Material( type: MaterialType.transparency, child: InkWell( child: Container( width: widget.arrowsWidth, alignment: Alignment.bottomCenter, child: Icon(Icons.arrow_left_outlined, size: widget.arrowsWidth)), onTap: _canGoDown ? () => _update(false) : null)), suffixIcon: Material( type: MaterialType.transparency, child: InkWell( child: Container( width: widget.arrowsWidth, alignment: Alignment.bottomCenter, child: Icon(Icons.arrow_right_outlined, size: widget.arrowsWidth)), onTap: _canGoUp ? () => _update(true) : null))), maxLines: 1, onChanged: (value) { final intValue = int.tryParse(value); widget.onChanged?.call(intValue); _updateArrows(intValue); }, inputFormatters: [_NumberTextInputFormatter(widget.min, widget.max)]); void _update(bool up) { var intValue = int.tryParse(_controller.text); intValue == null ? intValue = widget.min : intValue += up ? widget.step : -widget.step; intValue = intValue.clamp(widget.min, widget.max); // Ensure intValue is within range _controller.text = intValue.toString(); // Manually call the onChanged callback after updating the controller's text widget.onChanged?.call(intValue); _updateArrows(intValue); _focusNode.requestFocus(); } void _updateArrows(int? value) { final canGoUp = value == null || value < widget.max; final canGoDown = value == null || value > widget.min; if (_canGoUp != canGoUp || _canGoDown != canGoDown) setState(() { _canGoUp = canGoUp; _canGoDown = canGoDown; }); } } class _NumberTextInputFormatter extends TextInputFormatter { final int min; final int max; _NumberTextInputFormatter(this.min, this.max); @override TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { if (const ['-', ''].contains(newValue.text)) return newValue; final intValue = int.tryParse(newValue.text); if (intValue == null) return oldValue; if (intValue < min) return newValue.copyWith(text: min.toString()); if (intValue > max) return newValue.copyWith(text: max.toString()); return newValue.copyWith(text: intValue.toString()); } }