import 'dart:math' as math; import 'package:flutter/material.dart'; // How far the glow bleeds outside the child's bounds on each side. const _kGlowSpread = 12.0; /// Wraps any widget with a soft outward Gemini-colored glow that animates /// while [isProcessing] is true and disappears when false. /// The glow paints *outside* the child's bounds without affecting layout. class GeminiAnimatedBorder extends StatefulWidget { final Widget child; final bool isProcessing; /// Must match the border-radius of the wrapped widget so the glow follows its shape. final double borderRadius; /// When true the glow uses DeepSeek blue tones instead of Gemini colours. final bool useDeepSeekColors; const GeminiAnimatedBorder({ super.key, required this.child, required this.isProcessing, this.borderRadius = 8, this.useDeepSeekColors = false, }); @override State createState() => _GeminiAnimatedBorderState(); } class _GeminiAnimatedBorderState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(seconds: 3), ); if (widget.isProcessing) _controller.repeat(); } @override void didUpdateWidget(GeminiAnimatedBorder old) { super.didUpdateWidget(old); if (widget.isProcessing && !old.isProcessing) { _controller.repeat(); } else if (!widget.isProcessing && old.isProcessing) { _controller.stop(); _controller.reset(); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (!widget.isProcessing) return widget.child; return AnimatedBuilder( animation: _controller, child: widget.child, builder: (context, child) { return Stack( clipBehavior: Clip.none, children: [ // Glow layer positioned to bleed outside the child on all sides. Positioned( left: -_kGlowSpread, right: -_kGlowSpread, top: -_kGlowSpread, bottom: -_kGlowSpread, child: CustomPaint( painter: _GeminiGlowPainter( rotation: _controller.value, borderRadius: widget.borderRadius, glowSpread: _kGlowSpread, deepSeekMode: widget.useDeepSeekColors, ), ), ), child!, ], ); }, ); } } /// Wraps a TextField with a rotating gradient border that animates with Gemini colors /// when processing is active. Shows normal border appearance when not processing. class GeminiAnimatedTextField extends StatefulWidget { final TextEditingController controller; final String? labelText; final int? maxLines; final bool enabled; final bool isProcessing; final InputDecoration? decoration; /// When true the glow uses DeepSeek blue tones instead of Gemini colours. final bool useDeepSeekColors; const GeminiAnimatedTextField({ super.key, required this.controller, this.labelText, this.maxLines, this.enabled = true, this.isProcessing = false, this.decoration, this.useDeepSeekColors = false, }); @override State createState() => _GeminiAnimatedTextFieldState(); } class _GeminiAnimatedTextFieldState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(seconds: 3), ); if (widget.isProcessing) _controller.repeat(); } @override void didUpdateWidget(GeminiAnimatedTextField oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isProcessing && !oldWidget.isProcessing) { _controller.repeat(); } else if (!widget.isProcessing && oldWidget.isProcessing) { _controller.stop(); _controller.reset(); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final field = TextField( controller: widget.controller, enabled: widget.enabled && !widget.isProcessing, maxLines: widget.maxLines, decoration: widget.decoration ?? InputDecoration(labelText: widget.labelText), ); if (!widget.isProcessing) return field; return AnimatedBuilder( animation: _controller, child: field, builder: (context, child) { return Stack( clipBehavior: Clip.none, children: [ Positioned( left: -_kGlowSpread, right: -_kGlowSpread, top: -_kGlowSpread, bottom: -_kGlowSpread, child: CustomPaint( painter: _GeminiGlowPainter( rotation: _controller.value, borderRadius: 4, glowSpread: _kGlowSpread, deepSeekMode: widget.useDeepSeekColors, ), ), ), child!, ], ); }, ); } } /// Paints a soft outward glow using layered blurred strokes. /// The [size] passed to [paint] is LARGER than the child by [glowSpread] on /// every side, so the rrect is inset by [glowSpread] to sit exactly on the /// child's border, and the blur bleeds outward. class _GeminiGlowPainter extends CustomPainter { final double rotation; final double borderRadius; final double glowSpread; final bool deepSeekMode; // Gemini brand colors — closed loop for a seamless sweep. static const _geminiColors = [ Color(0xFF4285F4), // Blue Color(0xFFEA4335), // Red Color(0xFFFBBC04), // Yellow Color(0xFF34A853), // Green Color(0xFF4285F4), // Blue (close loop) ]; static const _geminiStops = [0.0, 0.25, 0.5, 0.75, 1.0]; // DeepSeek brand colors — pure blue closed loop. static const _deepSeekColors = [ Color(0xFF4D9BFF), // Sky blue Color(0xFF1A56DB), // Deep blue Color(0xFF00CFFF), // Cyan Color(0xFF4D9BFF), // Sky blue (close loop) ]; static const _deepSeekStops = [0.0, 0.33, 0.66, 1.0]; const _GeminiGlowPainter({ required this.rotation, required this.borderRadius, required this.glowSpread, this.deepSeekMode = false, }); @override void paint(Canvas canvas, Size size) { // The rect that coincides with the actual child's outline. final childRect = Rect.fromLTWH( glowSpread, glowSpread, size.width - glowSpread * 2, size.height - glowSpread * 2, ); final rrect = RRect.fromRectAndRadius( childRect, Radius.circular(borderRadius), ); final colors = deepSeekMode ? _deepSeekColors : _geminiColors; final stops = deepSeekMode ? _deepSeekStops : _geminiStops; final shader = SweepGradient( colors: colors, stops: stops, transform: GradientRotation(rotation * 2 * math.pi), ).createShader(childRect); // Outer glow — wide stroke + strong blur spreads the color outward. canvas.drawRRect( rrect, Paint() ..shader = shader ..style = PaintingStyle.stroke ..strokeWidth = glowSpread * 1.6 ..maskFilter = MaskFilter.blur(BlurStyle.normal, glowSpread), ); // Inner glow — narrower, less blurred for a crisper halo near the border. canvas.drawRRect( rrect, Paint() ..shader = shader ..style = PaintingStyle.stroke ..strokeWidth = glowSpread * 0.7 ..maskFilter = MaskFilter.blur(BlurStyle.normal, glowSpread * 0.4), ); } @override bool shouldRepaint(_GeminiGlowPainter old) => old.rotation != rotation || old.deepSeekMode != deepSeekMode; }