import 'dart:math' as math; import 'package:flutter/material.dart'; /// A circular gauge widget for displaying percentage values. /// /// Commonly used for system stats like CPU, Memory, Disk usage. class GaugeWidget extends StatelessWidget { const GaugeWidget({ super.key, required this.value, required this.label, this.size = 120, this.strokeWidth = 10, this.color, this.backgroundColor, this.showPercentage = true, this.icon, }); /// The value to display (0.0 - 1.0). final double value; /// Label displayed below the gauge. final String label; /// Size of the gauge widget. final double size; /// Width of the gauge arc stroke. final double strokeWidth; /// Color of the filled arc. Uses primary color if not specified. final Color? color; /// Color of the background arc. Uses surfaceVariant if not specified. final Color? backgroundColor; /// Whether to show the percentage text in the center. final bool showPercentage; /// Optional icon to show above the percentage. final IconData? icon; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final effectiveColor = color ?? colorScheme.primary; final effectiveBackgroundColor = backgroundColor ?? colorScheme.surfaceContainerHighest; // Clamp value between 0 and 1 final clampedValue = value.clamp(0.0, 1.0); return SizedBox( width: size, height: size + 24, // Extra space for label child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: size, height: size, child: CustomPaint( painter: _GaugePainter( value: clampedValue, color: effectiveColor, backgroundColor: effectiveBackgroundColor, strokeWidth: strokeWidth, ), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon( icon, size: size * 0.2, color: effectiveColor, ), SizedBox(height: size * 0.02), ], if (showPercentage) Text( '${(clampedValue * 100).round()}%', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, fontSize: size * 0.18, ), ), ], ), ), ), ), const SizedBox(height: 8), Text( label, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ); } } class _GaugePainter extends CustomPainter { _GaugePainter({ required this.value, required this.color, required this.backgroundColor, required this.strokeWidth, }); final double value; final Color color; final Color backgroundColor; final double strokeWidth; @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = (size.width - strokeWidth) / 2; // Start from top (-90 degrees) and sweep clockwise const startAngle = -math.pi / 2; const sweepAngle = 2 * math.pi; // Background arc final backgroundPaint = Paint() ..color = backgroundColor ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.round; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, backgroundPaint, ); // Value arc if (value > 0) { final valuePaint = Paint() ..color = color ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.round; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle * value, false, valuePaint, ); } } @override bool shouldRepaint(_GaugePainter oldDelegate) { return oldDelegate.value != value || oldDelegate.color != color || oldDelegate.backgroundColor != backgroundColor || oldDelegate.strokeWidth != strokeWidth; } } /// A row of gauge widgets with consistent sizing. class GaugeRow extends StatelessWidget { const GaugeRow({ super.key, required this.gauges, this.gaugeSize = 100, }); final List gauges; final double gaugeSize; @override Widget build(BuildContext context) { return Wrap( spacing: 16, runSpacing: 16, alignment: WrapAlignment.center, children: gauges .map( (data) => GaugeWidget( value: data.value, label: data.label, size: gaugeSize, color: data.color, icon: data.icon, ), ) .toList(), ); } } /// Data class for gauge configuration. class GaugeData { const GaugeData({ required this.value, required this.label, this.color, this.icon, }); final double value; final String label; final Color? color; final IconData? icon; }