diff --git a/CHANGELOG.md b/CHANGELOG.md index 743c60a4..dc772a8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [0.0.3] +* Implemented `ProgressCircle` and `ProgressBar` * Implemented the `Switch` widget ## [0.0.2] diff --git a/lib/macos_ui.dart b/lib/macos_ui.dart index ae8033d0..8d49d5d1 100644 --- a/lib/macos_ui.dart +++ b/lib/macos_ui.dart @@ -20,4 +20,5 @@ export 'src/util.dart'; export 'src/styles/theme.dart'; export 'src/styles/typography.dart'; export 'src/layout/scaffold.dart'; -export 'src/buttons/switch.dart'; \ No newline at end of file +export 'src/buttons/switch.dart'; +export 'src/indicators/progress_indicators.dart'; \ No newline at end of file diff --git a/lib/src/indicators/progress_indicators.dart b/lib/src/indicators/progress_indicators.dart new file mode 100644 index 00000000..70b5742e --- /dev/null +++ b/lib/src/indicators/progress_indicators.dart @@ -0,0 +1,232 @@ +import 'dart:math' as math; + +import 'package:macos_ui/macos_ui.dart'; + +import 'package:flutter/cupertino.dart' as c; + +/// A [ProgressCircle] show progress in a circular form, +/// either as a spinner or as a circle that fills in as +/// progress continues. +class ProgressCircle extends StatelessWidget { + /// Creates a new progress circle. + /// + /// [radius] must be non-negative + /// + /// [value] must be in the range of 0 and 100 + const ProgressCircle({ + Key? key, + this.value, + this.radius = 10, + this.innerColor, + this.borderColor, + }) : assert(value == null || value >= 0 && value <= 100), + assert(radius >= 0), + super(key: key); + + /// The value of the progress circle. If non-null, this has to + /// be non-negative and less the 100. If null, the progress circle + /// will be considered indeterminate, backed by [c.CupertinoActivityIndicator] + final double? value; + + /// The radius of the progress circle. Defaults to 10px + final double radius; + + /// The color of the circle at the middle. If null, + /// [CupertinoColors.secondarySystemFill] is used + final Color? innerColor; + + /// The color of the border. If null, [CupertinoColors.secondarySystemFill] + /// is used + final Color? borderColor; + + /// Whether the progress circle is determinate or not + bool get isDeterminate => value != null; + + @override + Widget build(BuildContext context) { + if (isDeterminate) { + return Semantics( + value: value!.toStringAsFixed(2), + child: SizedBox( + height: radius * 2, + width: radius * 2, + child: CustomPaint( + painter: _DeterminateCirclePainter( + value!, + innerColor: CupertinoDynamicColor.resolve( + innerColor ?? CupertinoColors.secondarySystemFill, + context, + ), + borderColor: CupertinoDynamicColor.resolve( + borderColor ?? CupertinoColors.secondarySystemFill, + context, + ), + ), + ), + ), + ); + } else { + return c.CupertinoActivityIndicator( + radius: radius, + ); + } + } +} + +class _DeterminateCirclePainter extends CustomPainter { + final double value; + + final Color? innerColor; + final Color? borderColor; + + const _DeterminateCirclePainter( + this.value, { + this.innerColor, + this.borderColor, + }); + + static const double _twoPi = math.pi * 2.0; + static const double _epsilon = .001; + static const double _sweep = _twoPi - _epsilon; + static const double _startAngle = -math.pi / 2.0; + + @override + void paint(Canvas canvas, Size size) { + /// Draw an arc + void drawArc( + double value, { + Paint? paint, + bool useCenter = true, + }) { + canvas.drawArc( + Offset.zero & size, + _startAngle, + (value / 100).clamp(0, 1) * _sweep, + useCenter, + paint ?? Paint() + ..color = innerColor ?? CupertinoColors.activeBlue, + ); + } + + // Draw the inner circle + drawArc(value); + + /// Draw the border + drawArc( + 100, + useCenter: false, + paint: Paint() + ..color = borderColor ?? CupertinoColors.activeBlue + ..style = PaintingStyle.stroke, + ); + } + + @override + bool shouldRepaint(_DeterminateCirclePainter old) => value != old.value; + + @override + bool shouldRebuildSemantics(_DeterminateCirclePainter oldDelegate) => false; +} + +/// A [ProgressBar] show progress in a horizontal bar. +class ProgressBar extends StatelessWidget { + /// Creates a new progress bar + /// + /// [height] more be non-negative + /// + /// [value] must be in the range of 0 and 100 + const ProgressBar({ + Key? key, + this.height = 4.5, + required this.value, + this.trackColor, + this.backgroundColor, + }) : assert(value >= 0 && value <= 100), + assert(height >= 0), + super(key: key); + + /// The value of the progress bar. If non-null, this has to + /// be non-negative and less the 100. If null, the progress bar + /// will be considered indeterminate. + final double value; + + /// The height of the line. Default to 4.5px + final double height; + + /// The color of the track. If null, [Style.accentColor] is used + final Color? trackColor; + + /// The color of the background. If null, [CupertinoColors.secondarySystemFill] + /// is used + final Color? backgroundColor; + + @override + Widget build(BuildContext context) { + return Semantics( + value: value.toStringAsFixed(2), + child: Container( + constraints: BoxConstraints( + minHeight: height, + maxHeight: height, + minWidth: 85, + ), + child: CustomPaint( + painter: _DeterminateBarPainter( + value, + activeColor: trackColor ?? + context.maybeStyle?.accentColor ?? + CupertinoColors.activeBlue, + backgroundColor: CupertinoDynamicColor.resolve( + backgroundColor ?? CupertinoColors.secondarySystemFill, + context, + ), + ), + ), + ), + ); + } +} + +class _DeterminateBarPainter extends CustomPainter { + final double value; + + final Color? backgroundColor; + final Color? activeColor; + + const _DeterminateBarPainter( + this.value, { + this.backgroundColor, + this.activeColor, + }); + + @override + void paint(Canvas canvas, Size size) { + // Draw the background line + canvas.drawRRect( + BorderRadius.circular(100).toRRect( + Offset.zero & size, + ), + Paint() + ..color = backgroundColor ?? CupertinoColors.secondarySystemFill + ..style = PaintingStyle.fill, + ); + + // Draw the active tick line + canvas.drawRRect( + BorderRadius.horizontal(left: Radius.circular(100)).toRRect(Offset.zero & + Size( + (value / 100).clamp(0.0, 1.0) * size.width, + size.height, + )), + Paint() + ..color = activeColor ?? CupertinoColors.activeBlue + ..style = PaintingStyle.fill, + ); + } + + @override + bool shouldRepaint(_DeterminateBarPainter old) => old.value != value; + + @override + bool shouldRebuildSemantics(_DeterminateBarPainter oldDelegate) => false; +}