using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; using System.Windows.Input; using InABox.Core; namespace InABox.Avalonia.Components; public class CircularCountdownTimer : Control { private readonly DispatcherTimer _timer; private DateTime? _startTime; public static readonly StyledProperty ImageProperty = AvaloniaProperty.Register(nameof(Image)); public IImage? Image { get => GetValue(ImageProperty); set => SetValue(ImageProperty, value); } public static readonly StyledProperty IsActiveProperty = AvaloniaProperty.Register(nameof(IsActive)); public bool IsActive { get => GetValue(IsActiveProperty); set => SetValue(IsActiveProperty, value); } public static readonly StyledProperty StartedProperty = AvaloniaProperty.Register(nameof(Started)); public ICommand? Started { get => GetValue(StartedProperty); set => SetValue(StartedProperty, value); } public static readonly StyledProperty StoppedProperty = AvaloniaProperty.Register(nameof(Stopped)); public ICommand? Stopped { get => GetValue(StoppedProperty); set => SetValue(StoppedProperty, value); } public static readonly StyledProperty BackgroundProperty = AvaloniaProperty.Register(nameof(Background), Brushes.Transparent); public IBrush Background { get => GetValue(BackgroundProperty); set => SetValue(BackgroundProperty, value); } public static readonly StyledProperty StrokeThicknessProperty = AvaloniaProperty.Register(nameof(StrokeThickness), 10.0); public double StrokeThickness { get => GetValue(StrokeThicknessProperty); set => SetValue(StrokeThicknessProperty, value); } public static readonly StyledProperty ProgressBackgroundProperty = AvaloniaProperty.Register(nameof(ProgressBackground), Brushes.Gray); public IBrush ProgressBackground { get => GetValue(ProgressBackgroundProperty); set => SetValue(ProgressBackgroundProperty, value); } public static readonly StyledProperty ProgressForegroundProperty = AvaloniaProperty.Register(nameof(ProgressForeground), Brushes.WhiteSmoke); public IBrush ProgressForeground { get => GetValue(ProgressForegroundProperty); set => SetValue(ProgressForegroundProperty, value); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == IsActiveProperty) { if (Equals(change.NewValue,true)) Start(); else Stop(); } } public static readonly StyledProperty DurationProperty = AvaloniaProperty.Register(nameof(Duration), 15.0); public double Duration { get => GetValue(DurationProperty); set { SetValue(DurationProperty, value); InvalidateVisual(); } } public CircularCountdownTimer() { _timer = new DispatcherTimer(DispatcherPriority.MaxValue) { Interval = TimeSpan.FromMilliseconds(200), }; _timer.Tick += TimerTick; } private void Start() { _startTime = DateTime.Now; _timer.Start(); Started?.Execute(DataContext); InvalidateVisual(); } private void Stop() { _timer.Stop(); _startTime = null; Stopped?.Execute(DataContext); InvalidateVisual(); } private TimeSpan RemainingTime() { if (!_startTime.HasValue) return TimeSpan.Zero; var _nowTime = DateTime.Now; var _endTime = _startTime.Value.AddSeconds(Duration); return _endTime > _nowTime ? _endTime - _nowTime : TimeSpan.Zero; } private void TimerTick(object? sender, EventArgs e) { if (RemainingTime() <= TimeSpan.Zero) IsActive = false; else InvalidateVisual(); } public override void Render(DrawingContext context) { var boundsRect = new Rect(Bounds.Left+Margin.Left, Bounds.Top+Margin.Top, Bounds.Width-(Margin.Left+Margin.Right), Bounds.Height-(Margin.Top+Margin.Bottom)); double centerX = boundsRect.Width / 2; double centerY = boundsRect.Height / 2; double radius = Math.Min(centerX, centerY) - StrokeThickness; var backgroundPen = new Pen(ProgressBackground, StrokeThickness); var progressPen = new Pen(ProgressForeground, StrokeThickness) { LineCap = PenLineCap.Flat // Smooth arc edges }; context.FillRectangle(Background, boundsRect); // Draw background circle context.DrawEllipse(null, backgroundPen, new Point(centerX, centerY), radius, radius); if (Image != null) { var boxwidth = Math.Sqrt(2) * radius; Rect rect = new Rect(centerX - (boxwidth/2), centerY - (boxwidth/2), boxwidth, boxwidth); context.DrawImage(Image, rect); } double _remainingTime = RemainingTime().TotalSeconds; if (_remainingTime.IsEffectivelyGreaterThan(0.0)) { // Calculate the sweep angle double sweepAngle = (_remainingTime / Duration) * 360; double startAngle = -90; double endAngle = startAngle + sweepAngle; // Convert angles to radians double startRad = Math.PI * startAngle / 180.0; double endRad = Math.PI * endAngle / 180.0; // Calculate points on the circle var startPoint = new Point( centerX + radius * Math.Cos(startRad), centerY + radius * Math.Sin(startRad) ); var endPoint = new Point( centerX + radius * Math.Cos(endRad), centerY + radius * Math.Sin(endRad) ); // Create an arc geometry var geometry = new StreamGeometry(); using (var ctx = geometry.Open()) { ctx.BeginFigure(startPoint, false); ctx.ArcTo(endPoint, new Size(radius, radius), 0, sweepAngle > 180, SweepDirection.Clockwise); } // Draw the progress arc context.DrawGeometry(null, progressPen, geometry); } } }