Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ProgressRing #6003

Closed
wants to merge 10 commits into from
1 change: 1 addition & 0 deletions samples/ControlCatalog/MainView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
<TabItem Header="OpenGL"><pages:OpenGlPage/></TabItem>
<TabItem Header="Pointers (Touch)"><pages:PointersPage/></TabItem>
<TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem>
<TabItem Header="ProgressRing"><pages:ProgressRingPage/></TabItem>
<TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
<TabItem Header="RelativePanel"><pages:RelativePanelPage/></TabItem>
<TabItem Header="ScrollViewer"><pages:ScrollViewerPage/></TabItem>
Expand Down
32 changes: 32 additions & 0 deletions samples/ControlCatalog/Pages/ProgressRingPage.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.ProgressRingPage">
<DockPanel>
<StackPanel Orientation="Vertical" Spacing="4" DockPanel.Dock="Top">
<TextBlock Classes="h1">ProgressRing</TextBlock>
<TextBlock Classes="h2">A progress bar-like control which is actually a doughnut shaped like a bagel</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="10" Margin="10,16" DockPanel.Dock="Top">
<CheckBox x:Name="isIndeterminate" Content="Toggle Indeterminate" />
<CheckBox x:Name="preserveAspect" Content="Toggle Preserve Aspect Ratio" IsChecked="True"/>
</StackPanel>
<Grid RowDefinitions="*,2*">
<DockPanel Grid.Row="0">
<Slider Name="height" Maximum="768" Minimum="200" Value="220" DockPanel.Dock="Bottom" Margin="0,20,0,0"/>
<Slider Name="progress" Maximum="100" Value="40" DockPanel.Dock="Top"/>
<ProgressRing Margin="20" PreserveAspect="{Binding #preserveAspect.IsChecked}" IsIndeterminate="{Binding #isIndeterminate.IsChecked}" Value="{Binding #progress.Value}" Minimum="{Binding #progress.Minimum}" Maximum="{Binding #progress.Maximum}"/>
</DockPanel>
<UniformGrid Grid.Row="1" Rows="2" Margin="10,0" Height="{Binding #height.Value}">
<ProgressRing IsIndeterminate="True" Margin="10"/>
<ProgressRing IsIndeterminate="True" PreserveAspect="False" Margin="10"/>
<ProgressRing Margin="10" Value="50" Minimum="0" Maximum="75"/>
<ProgressRing PreserveAspect="False" Margin="10" Value="50" Minimum="0" Maximum="75"/>
</UniformGrid>
</Grid>
</DockPanel>
<!--StackPanel Orientation="Vertical">
<Arc StrokeThickness="9" Stroke="Red" Width="{Binding #width.Value}" Height="{Binding #height.Value}" StartAngle="{Binding #start.Value}" EndAngle="{Binding #end.Value}"/>
<Slider x:Name="width" Minimum="0" Maximum="100" Value="40" Margin="0,10,0,0"/>
<Slider x:Name="height" Minimum="0" Maximum="100" Value="40" Margin="0,0,0,10"/>
<Slider x:Name="start" Minimum="0" Maximum="360" Value="0"/>
<Slider x:Name="end" Minimum="0" Maximum="360" Value="0"/>
</StackPanel-->
</UserControl>
18 changes: 18 additions & 0 deletions samples/ControlCatalog/Pages/ProgressRingPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace ControlCatalog.Pages
{
public class ProgressRingPage : UserControl
{
public ProgressRingPage()
{
this.InitializeComponent();
}

private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Media;

namespace Avalonia.Controls.Converters
{
public class FitSquarelyWithinAspectRatioConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Rect bounds = (Rect)value;
return Math.Min(bounds.Width, bounds.Height);
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
102 changes: 102 additions & 0 deletions src/Avalonia.Controls/ProgressRing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;

namespace Avalonia.Controls
{
/// <summary>
/// A control used to indicate the progress of an operation.
/// </summary>
[PseudoClasses(":preserveaspect", ":indeterminate")]
Splitwirez marked this conversation as resolved.
Show resolved Hide resolved
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), -90.0);

static ProgressRing()
{
MinimumProperty.Changed.AddClassHandler<ProgressRing>(OnRangePropertiesChanged);
ValueProperty.Changed.AddClassHandler<ProgressRing>(OnRangePropertiesChanged);
MaximumProperty.Changed.AddClassHandler<ProgressRing>(OnRangePropertiesChanged);
}

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this property was added only to simplify templating, it makes sense to move it to inner property with relevant name.
See https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Controls/ProgressBar.cs#L151
So it's it will be like "TemplateProperties.ValueAngle"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meaning that developers shouldn't try to use it elsewhere

{
get => GetValue(ValueAngleProperty);
private set => SetValue(ValueAngleProperty, value);
}

protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);

if (change.Property == IsIndeterminateProperty)
{
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>(), null);
}
else if (change.Property == PreserveAspectProperty)
{
UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault<bool>());
}
}

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 OnRangePropertiesChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
{
double min = sender.Minimum;
double ringVal = sender.Value;
double max = sender.Maximum;

if ((e.NewValue != null) && (e.NewValue is double newPropVal))
{
if (e.Property == MinimumProperty)
min = newPropVal;
else if (e.Property == ValueProperty)
ringVal = newPropVal;
else if (e.Property == MaximumProperty)
max = newPropVal;
}

sender.ValueAngle = (((ringVal - min) / (max - min)) * 360.0) - 90;
}
}
}
153 changes: 153 additions & 0 deletions src/Avalonia.Controls/Shapes/Arc.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using System;
using Avalonia.Media;

namespace Avalonia.Controls.Shapes
{
public class Arc : Shape
maxkatz6 marked this conversation as resolved.
Show resolved Hide resolved
{
ArcSegment _arcSegment;
PathFigure _arcFigure;
PathGeometry _arcGeometry;

PathGeometry _emptyGeometry = new PathGeometry()
{
Figures =
{
new PathFigure()
{
StartPoint = new Point(0, 0)
}
}
};

public static readonly StyledProperty<double> StartAngleProperty =
AvaloniaProperty.Register<Arc, double>(nameof(StartAngle), 0.0);

public static readonly StyledProperty<double> EndAngleProperty =
AvaloniaProperty.Register<Arc, double>(nameof(EndAngle), 0.0);

static Arc()
{
StrokeThicknessProperty.OverrideDefaultValue<Arc>(1);
AffectsGeometry<Arc>(BoundsProperty, StrokeThicknessProperty, StartAngleProperty, EndAngleProperty);
}

public Arc() : base()
{
_arcSegment = new ArcSegment()
{
SweepDirection = SweepDirection.Clockwise
};

_arcFigure = new PathFigure()
{
Segments =
{
_arcSegment
},
IsClosed = false
};

_arcGeometry = new PathGeometry()
{
Figures =
{
_arcFigure
}
};
}

public double StartAngle
{
get { return GetValue(StartAngleProperty); }
set { SetValue(StartAngleProperty, value); }
}

public double EndAngle
{
get { return GetValue(EndAngleProperty); }
set { SetValue(EndAngleProperty, value); }
}

protected override Geometry CreateDefiningGeometry()
{
double angle1 = DegreesToRad(StartAngle);
double angle2 = DegreesToRad(EndAngle);

double startAngle = Math.Min(angle1, angle2);
double endAngle = Math.Max(angle1, angle2);

double normStart = RadToNormRad(startAngle);
double normEnd = RadToNormRad(endAngle);

var rect = new Rect(Bounds.Size);

if ((normStart == normEnd) && (startAngle != endAngle)) //complete ring
{
return new EllipseGeometry(rect.Deflate(StrokeThickness / 2));
}
else if ((normStart == normEnd) && (startAngle == endAngle)) //empty
{
return _emptyGeometry;
}
else //partial ring
{
var deflatedRect = rect.Deflate(StrokeThickness / 2);

double centerX = rect.Center.X;
double centerY = rect.Center.Y;

double radiusX = deflatedRect.Width / 2;
double radiusY = deflatedRect.Height / 2;

double angleGap = RadToNormRad(endAngle - startAngle);

Point startPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, startAngle);
Point endPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, endAngle);

_arcFigure.StartPoint = startPoint;

_arcSegment.Point = endPoint;
_arcSegment.IsLargeArc = angleGap >= HALF_TAU;
_arcSegment.Size = new Size(radiusX, radiusY);

return _arcGeometry;
}
}

public override void Render(DrawingContext ctx)
{
double angle1 = DegreesToRad(StartAngle);
double angle2 = DegreesToRad(EndAngle);

double startAngle = Math.Min(angle1, angle2);
double endAngle = Math.Max(angle1, angle2);

double normStart = RadToNormRad(startAngle);
double normEnd = RadToNormRad(endAngle);


if ((normStart == normEnd) && (startAngle == endAngle)) //empty
{

}
else //not empty
{
base.Render(ctx);
}
}

const double TAU = 6.2831853071795862; //Math.Tau doesn't exist pre-.NET 5 :(
const double HALF_TAU = TAU / 2.0;

static double DegreesToRad(double inAngle) =>
inAngle * Math.PI / 180;

static double RadToNormRad(double inAngle) =>
(0 + (inAngle % TAU) + TAU) % TAU;


static Point GetRingPoint(double radiusX, double radiusY, double centerX, double centerY, double angle) =>
new Point((radiusX * Math.Cos(angle)) + centerX, (radiusY * Math.Sin(angle)) + centerY);
}
}
1 change: 1 addition & 0 deletions src/Avalonia.Themes.Default/DefaultTheme.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<StyleInclude Source="resm:Avalonia.Themes.Default.PathIcon.xaml?assembly=Avalonia.Themes.Default" />
<StyleInclude Source="resm:Avalonia.Themes.Default.PopupRoot.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ProgressBar.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ProgressRing.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.RadioButton.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.RepeatButton.xaml?assembly=Avalonia.Themes.Default" />
<StyleInclude Source="resm:Avalonia.Themes.Default.Separator.xaml?assembly=Avalonia.Themes.Default"/>
Expand Down
Loading