import 'dart:math' as math; import 'package:flutter/material.dart'; /// A speedometer-style 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); // Height is smaller since we only draw half circle final gaugeHeight = size * 0.6; final iconSpace = icon != null ? size * 0.36 : 0.0; return SizedBox( width: size, height: gaugeHeight + 32 + iconSpace, // Space for label + icon child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: size, height: gaugeHeight, child: CustomPaint( painter: _GaugePainter( value: clampedValue, color: effectiveColor, backgroundColor: effectiveBackgroundColor, strokeWidth: strokeWidth, ), child: Align( alignment: const Alignment(0, 0.6), child: showPercentage ? Text( '${(clampedValue * 100).round()}%', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, fontSize: size * 0.2, ), ) : const SizedBox.shrink(), ), ), ), const SizedBox(height: 4), Text( label, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), if (icon != null) ...[ const SizedBox(height: 8), Icon( icon, size: size * 0.28, color: effectiveColor, ), ], ], ), ); } } 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) { // Center at bottom of widget for speedometer style final center = Offset(size.width / 2, size.height); final radius = (size.width - strokeWidth) / 2; // Speedometer arc: starts from left (180°) and sweeps 180° to right const startAngle = math.pi; // 180 degrees (left side) const sweepAngle = math.pi; // 180 degrees sweep (semicircle) // 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: 24, runSpacing: 16, alignment: WrapAlignment.center, children: gauges .map( (data) => GaugeWidget( value: data.value, label: data.label, size: gaugeSize, strokeWidth: gaugeSize * 0.1, 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; }