Ionite
1 year ago
3 changed files with 262 additions and 0 deletions
@ -0,0 +1,129 @@ |
|||||||
|
using Avalonia; |
||||||
|
using Avalonia.Controls; |
||||||
|
using Avalonia.Controls.Metadata; |
||||||
|
using Avalonia.Controls.Primitives; |
||||||
|
using Avalonia.Layout; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// A control used to indicate the progress of an operation. |
||||||
|
/// </summary> |
||||||
|
[PseudoClasses(":preserveaspect", ":indeterminate")] |
||||||
|
public class ProgressRing : RangeBase |
||||||
|
{ |
||||||
|
public static readonly StyledProperty<bool> IsIndeterminateProperty = |
||||||
|
ProgressBar.IsIndeterminateProperty.AddOwner<ProgressRing>(); |
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> PreserveAspectProperty = |
||||||
|
AvaloniaProperty.Register<ProgressRing, bool>(nameof(PreserveAspect), true); |
||||||
|
|
||||||
|
public static readonly StyledProperty<double> ValueAngleProperty = |
||||||
|
AvaloniaProperty.Register<ProgressRing, double>(nameof(ValueAngle), 0); |
||||||
|
|
||||||
|
public static readonly StyledProperty<double> StartAngleProperty = |
||||||
|
AvaloniaProperty.Register<ProgressRing, double>(nameof(StartAngle), 0); |
||||||
|
|
||||||
|
public static readonly StyledProperty<double> EndAngleProperty = |
||||||
|
AvaloniaProperty.Register<ProgressRing, double>(nameof(EndAngle), 360); |
||||||
|
|
||||||
|
static ProgressRing() |
||||||
|
{ |
||||||
|
MinimumProperty.Changed.AddClassHandler<ProgressRing>(OnMinimumPropertyChanged); |
||||||
|
MaximumProperty.Changed.AddClassHandler<ProgressRing>(OnMaximumPropertyChanged); |
||||||
|
ValueProperty.Changed.AddClassHandler<ProgressRing>(OnValuePropertyChanged); |
||||||
|
MaximumProperty.Changed.AddClassHandler<ProgressRing>(OnStartAnglePropertyChanged); |
||||||
|
MaximumProperty.Changed.AddClassHandler<ProgressRing>(OnEndAnglePropertyChanged); |
||||||
|
} |
||||||
|
|
||||||
|
public ProgressRing() |
||||||
|
{ |
||||||
|
UpdatePseudoClasses(IsIndeterminate, PreserveAspect); |
||||||
|
} |
||||||
|
|
||||||
|
public bool IsIndeterminate |
||||||
|
{ |
||||||
|
get => GetValue(IsIndeterminateProperty); |
||||||
|
set => SetValue(IsIndeterminateProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public bool PreserveAspect |
||||||
|
{ |
||||||
|
get => GetValue(PreserveAspectProperty); |
||||||
|
set => SetValue(PreserveAspectProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public double ValueAngle |
||||||
|
{ |
||||||
|
get => GetValue(ValueAngleProperty); |
||||||
|
private set => SetValue(ValueAngleProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public double StartAngle |
||||||
|
{ |
||||||
|
get => GetValue(StartAngleProperty); |
||||||
|
set => SetValue(StartAngleProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public double EndAngle |
||||||
|
{ |
||||||
|
get => GetValue(EndAngleProperty); |
||||||
|
set => SetValue(EndAngleProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
||||||
|
{ |
||||||
|
base.OnPropertyChanged(change); |
||||||
|
var e = change as AvaloniaPropertyChangedEventArgs<bool>; |
||||||
|
if (e is null) return; |
||||||
|
|
||||||
|
if (e.Property == IsIndeterminateProperty) |
||||||
|
{ |
||||||
|
UpdatePseudoClasses(e.NewValue.GetValueOrDefault(), null); |
||||||
|
} |
||||||
|
else if (e.Property == PreserveAspectProperty) |
||||||
|
{ |
||||||
|
UpdatePseudoClasses(null, e.NewValue.GetValueOrDefault()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void UpdatePseudoClasses( |
||||||
|
bool? isIndeterminate, |
||||||
|
bool? preserveAspect) |
||||||
|
{ |
||||||
|
if (isIndeterminate.HasValue) |
||||||
|
{ |
||||||
|
PseudoClasses.Set(":indeterminate", isIndeterminate.Value); |
||||||
|
} |
||||||
|
|
||||||
|
if (preserveAspect.HasValue) |
||||||
|
{ |
||||||
|
PseudoClasses.Set(":preserveaspect", preserveAspect.Value); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static void OnMinimumPropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) |
||||||
|
{ |
||||||
|
sender.Minimum = (double)e.NewValue; |
||||||
|
} |
||||||
|
|
||||||
|
static void OnMaximumPropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) |
||||||
|
{ |
||||||
|
sender.Maximum = (double)e.NewValue; |
||||||
|
} |
||||||
|
|
||||||
|
static void OnValuePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) |
||||||
|
{ |
||||||
|
sender.ValueAngle = ((double)e.NewValue - sender.Minimum) * (sender.EndAngle - sender.StartAngle) / (sender.Maximum - sender.Minimum); |
||||||
|
} |
||||||
|
|
||||||
|
static void OnStartAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) |
||||||
|
{ |
||||||
|
sender.StartAngle = (double)e.NewValue; |
||||||
|
} |
||||||
|
|
||||||
|
static void OnEndAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) |
||||||
|
{ |
||||||
|
sender.EndAngle = (double)e.NewValue; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
using System; |
||||||
|
using System.Globalization; |
||||||
|
using Avalonia; |
||||||
|
using Avalonia.Data.Converters; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Converters; |
||||||
|
|
||||||
|
public class FitSquarelyWithinAspectRatioConverter : IValueConverter |
||||||
|
{ |
||||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||||
|
{ |
||||||
|
var bounds = value is Rect rect ? rect : default; |
||||||
|
return Math.Min(bounds.Width, bounds.Height); |
||||||
|
} |
||||||
|
|
||||||
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||||
|
{ |
||||||
|
throw new NotImplementedException(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,113 @@ |
|||||||
|
<Styles xmlns="https://github.com/avaloniaui" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||||
|
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" |
||||||
|
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||||
|
x:CompileBindings="True"> |
||||||
|
|
||||||
|
<Design.PreviewWith> |
||||||
|
<StackPanel> |
||||||
|
<Border Padding="20"> |
||||||
|
<controls:ProgressRing StartAngle="90" EndAngle="270" Value="50" Foreground="Red" BorderThickness="5" Width="100" Height="100"></controls:ProgressRing> |
||||||
|
</Border> |
||||||
|
<Border Padding="20"> |
||||||
|
<controls:ProgressRing IsIndeterminate="True" BorderThickness="10" Width="100" Height="100"></controls:ProgressRing> |
||||||
|
</Border> |
||||||
|
</StackPanel> |
||||||
|
</Design.PreviewWith> |
||||||
|
|
||||||
|
<Styles.Resources> |
||||||
|
<converters:FitSquarelyWithinAspectRatioConverter x:Key="FitSquarelyWithinAspectRatioConverter"/> |
||||||
|
</Styles.Resources> |
||||||
|
|
||||||
|
<Style Selector="controls|ProgressRing"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColor}"/> |
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" /> |
||||||
|
<Setter Property="BorderThickness" Value="9" /> |
||||||
|
<Setter Property="MinHeight" Value="16" /> |
||||||
|
<Setter Property="MinWidth" Value="16" /> |
||||||
|
<Setter Property="Template"> |
||||||
|
<ControlTemplate> |
||||||
|
<Panel x:Name="FluentRingRoot"> |
||||||
|
<Ellipse x:Name="Track" |
||||||
|
Stroke="{TemplateBinding Background}" |
||||||
|
StrokeThickness="{ReflectionBinding BorderThickness.Left, RelativeSource={RelativeSource Mode=TemplatedParent}}" /> |
||||||
|
<Arc x:Name="Fill" |
||||||
|
Stroke="{TemplateBinding Foreground}" |
||||||
|
StrokeThickness="{ReflectionBinding BorderThickness.Left, RelativeSource={RelativeSource Mode=TemplatedParent}}" |
||||||
|
StrokeLineCap="Round" /> |
||||||
|
</Panel> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="controls|ProgressRing:not(:indeterminate) /template/ Arc#Fill"> |
||||||
|
<Setter Property="StartAngle" Value="{ReflectionBinding StartAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/> |
||||||
|
<Setter Property="SweepAngle" Value="{ReflectionBinding ValueAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="controls|ProgressRing:preserveaspect"> |
||||||
|
<Setter Property="MinWidth" Value="32" /> |
||||||
|
<Setter Property="MinHeight" Value="32" /> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="controls|ProgressRing:preserveaspect /template/ Panel#FluentRingRoot"> |
||||||
|
<Setter Property="Width" Value="{TemplateBinding Bounds, Converter={StaticResource FitSquarelyWithinAspectRatioConverter}}"/> |
||||||
|
<Setter Property="Height" Value="{ReflectionBinding Width, RelativeSource={RelativeSource Mode=Self}}"/> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="controls|ProgressRing[IsEnabled=True]:indeterminate /template/ Arc#Fill"> |
||||||
|
<Style.Animations> |
||||||
|
<Animation Duration="0:0:5" Easing="LinearEasing" IterationCount="INFINITE" FillMode="Both"> |
||||||
|
<KeyFrame Cue="0%"> |
||||||
|
<Setter Property="StartAngle" Value="-720"/> |
||||||
|
<Setter Property="SweepAngle" Value="0"/> |
||||||
|
</KeyFrame> |
||||||
|
<KeyFrame Cue="12.5%"> |
||||||
|
<Setter Property="StartAngle" Value="-540"/> |
||||||
|
<Setter Property="SweepAngle" Value="50"/> |
||||||
|
</KeyFrame> |
||||||
|
<KeyFrame Cue="25%"> |
||||||
|
<Setter Property="StartAngle" Value="-360"/> |
||||||
|
<Setter Property="SweepAngle" Value="100"/> |
||||||
|
</KeyFrame> |
||||||
|
<KeyFrame Cue="32.5%"> |
||||||
|
<Setter Property="StartAngle" Value="-180"/> |
||||||
|
<Setter Property="SweepAngle" Value="50"/> |
||||||
|
</KeyFrame> |
||||||
|
<KeyFrame Cue="50%"> |
||||||
|
<Setter Property="StartAngle" Value="0"/> |
||||||
|
<Setter Property="SweepAngle" Value="5"/> |
||||||
|
</KeyFrame> |
||||||
|
<KeyFrame Cue="62.5%"> |
||||||
|
<Setter Property="StartAngle" Value="180"/> |
||||||
|
<Setter Property="SweepAngle" Value="50"/> |
||||||
|
</KeyFrame> |
||||||
|
<KeyFrame Cue="75%"> |
||||||
|
<Setter Property="StartAngle" Value="360"/> |
||||||
|
<Setter Property="SweepAngle" Value="100"/> |
||||||
|
</KeyFrame> |
||||||
|
<KeyFrame Cue="87.5%"> |
||||||
|
<Setter Property="StartAngle" Value="540"/> |
||||||
|
<Setter Property="SweepAngle" Value="50"/> |
||||||
|
</KeyFrame> |
||||||
|
<KeyFrame Cue="100%"> |
||||||
|
<Setter Property="StartAngle" Value="720"/> |
||||||
|
<Setter Property="SweepAngle" Value="0"/> |
||||||
|
</KeyFrame> |
||||||
|
</Animation> |
||||||
|
</Style.Animations> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="controls|ProgressRing[IsEnabled=True] /template/ Ellipse#Track"> |
||||||
|
<Style.Animations> |
||||||
|
<Animation Duration="0:0:1" IterationCount="INFINITE"> |
||||||
|
<KeyFrame Cue="0%"> |
||||||
|
<Setter Property="Opacity" Value="0.99"/> |
||||||
|
</KeyFrame> |
||||||
|
<KeyFrame Cue="100%"> |
||||||
|
<Setter Property="Opacity" Value="1"/> |
||||||
|
</KeyFrame> |
||||||
|
</Animation> |
||||||
|
</Style.Animations> |
||||||
|
</Style> |
||||||
|
</Styles> |
Loading…
Reference in new issue