// lib/ui/widgets/booster_hint.dart import 'package:flutter/material.dart'; import '../../game/models/booster.dart'; import '../../l10n/gen/app_localizations.dart'; /// A small floating pill that appears above the board while a targeted booster /// (hammer / line-bomb) is armed, telling the player what to tap. Glows with a /// gentle pulse in the season accent colour, slides/scales in, and cancels the /// armed state when tapped. Renders (invisibly, ignoring pointer) when nothing /// is armed so it can animate in/out in place without layout jumps. class BoosterHint extends StatefulWidget { const BoosterHint({ super.key, required this.arming, required this.accent, required this.onCancel, }); final BoosterType? arming; final Color accent; final VoidCallback onCancel; @override State createState() => _BoosterHintState(); } class _BoosterHintState extends State with SingleTickerProviderStateMixin { late final AnimationController _pulse = AnimationController( vsync: this, duration: const Duration(milliseconds: 1100), ); static const _icons = { BoosterType.hammer: Icons.gavel, BoosterType.shuffle: Icons.shuffle, BoosterType.lineBomb: Icons.clear_all, }; @override void initState() { super.initState(); _syncPulse(); } @override void didUpdateWidget(BoosterHint oldWidget) { super.didUpdateWidget(oldWidget); _syncPulse(); } /// Pulse only while a booster is armed — idle (no live ticker) otherwise, so /// it never spins during normal play or pins a timer open in widget tests. void _syncPulse() { if (widget.arming != null) { if (!_pulse.isAnimating) _pulse.repeat(reverse: true); } else if (_pulse.isAnimating) { _pulse ..stop() ..value = 0; } } @override void dispose() { _pulse.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final arming = widget.arming; final visible = arming != null; final l10n = AppLocalizations.of(context)!; final label = arming == BoosterType.lineBomb ? l10n.boosterTapLine : l10n.boosterTapTarget; final accent = widget.accent; return IgnorePointer( ignoring: !visible, child: AnimatedOpacity( opacity: visible ? 1 : 0, duration: const Duration(milliseconds: 160), child: AnimatedSlide( offset: visible ? Offset.zero : const Offset(0, -0.4), duration: const Duration(milliseconds: 220), curve: Curves.easeOutBack, child: GestureDetector( onTap: widget.onCancel, child: AnimatedBuilder( animation: _pulse, builder: (context, child) { final t = _pulse.value; // 0..1, breathing return Container( decoration: BoxDecoration( color: accent, borderRadius: BorderRadius.circular(999), boxShadow: [ BoxShadow( color: accent.withValues(alpha: 0.22 + 0.28 * t), blurRadius: 10 + 12 * t, spreadRadius: 1 + 3 * t, ), ], ), child: child, ); }, child: Padding( padding: const EdgeInsets.fromLTRB(7, 6, 12, 6), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 26, height: 26, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.22), shape: BoxShape.circle, ), child: Icon( _icons[arming ?? BoosterType.hammer], size: 16, color: Colors.white, ), ), const SizedBox(width: 8), Text( label, style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 8), const Icon(Icons.close, size: 16, color: Colors.white70), ], ), ), ), ), ), ), ); } }