From d1944b01c3f67d5d91241da01943679a7c6d40ae Mon Sep 17 00:00:00 2001
From: NaBian <836904362@qq.com>
Date: Sat, 13 Jun 2026 15:37:34 +0800
Subject: [PATCH] feat: Add CirclePanel control and demo.
Introduce a new CirclePanel control that arranges visible children evenly around a circle. The control exposes Diameter, KeepVertical and OffsetAngle styled properties and implements MeasureOverride/ArrangeOverride to position and optionally rotate items on a circular path. Also add a demo UserControl (CirclePanelDemo.axaml and .axaml.cs) with a themed button template to showcase the panel.
---
.../UserControl/Styles/CirclePanelDemo.axaml | 49 +++++++++
.../Styles/CirclePanelDemo.axaml.cs | 9 ++
.../Controls/Panel/CirclePanel.cs | 100 ++++++++++++++++++
3 files changed, 158 insertions(+)
create mode 100644 src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/CirclePanelDemo.axaml
create mode 100644 src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/CirclePanelDemo.axaml.cs
create mode 100644 src/Avalonia/HandyControl_Avalonia/Controls/Panel/CirclePanel.cs
diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/CirclePanelDemo.axaml b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/CirclePanelDemo.axaml
new file mode 100644
index 00000000..f5a35558
--- /dev/null
+++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/CirclePanelDemo.axaml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/CirclePanelDemo.axaml.cs b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/CirclePanelDemo.axaml.cs
new file mode 100644
index 00000000..76e722d7
--- /dev/null
+++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/CirclePanelDemo.axaml.cs
@@ -0,0 +1,9 @@
+namespace HandyControlDemo.UserControl;
+
+public partial class CirclePanelDemo : Avalonia.Controls.UserControl
+{
+ public CirclePanelDemo()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/Avalonia/HandyControl_Avalonia/Controls/Panel/CirclePanel.cs b/src/Avalonia/HandyControl_Avalonia/Controls/Panel/CirclePanel.cs
new file mode 100644
index 00000000..1380c243
--- /dev/null
+++ b/src/Avalonia/HandyControl_Avalonia/Controls/Panel/CirclePanel.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace HandyControl.Controls;
+
+public class CirclePanel : Panel
+{
+ public static readonly StyledProperty DiameterProperty =
+ AvaloniaProperty.Register(nameof(Diameter), defaultValue: 170.0);
+
+ public double Diameter
+ {
+ get => GetValue(DiameterProperty);
+ set => SetValue(DiameterProperty, value);
+ }
+
+ public static readonly StyledProperty KeepVerticalProperty =
+ AvaloniaProperty.Register(nameof(KeepVertical));
+
+ public bool KeepVertical
+ {
+ get => GetValue(KeepVerticalProperty);
+ set => SetValue(KeepVerticalProperty, value);
+ }
+
+ public static readonly StyledProperty OffsetAngleProperty =
+ AvaloniaProperty.Register(nameof(OffsetAngle), defaultValue: 0d);
+
+ public double OffsetAngle
+ {
+ get => GetValue(OffsetAngleProperty);
+ set => SetValue(OffsetAngleProperty, value);
+ }
+
+ static CirclePanel()
+ {
+ AffectsMeasure(DiameterProperty, KeepVerticalProperty, OffsetAngleProperty);
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ var diameter = Diameter;
+ var layoutChildren = Children.Where(element => element.IsVisible).ToList();
+
+ if (layoutChildren.Count == 0) return new Size(diameter, diameter);
+
+ var newSize = new Size(diameter, diameter);
+
+ foreach (var element in layoutChildren)
+ {
+ element.Measure(newSize);
+ }
+
+ return newSize;
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ var keepVertical = KeepVertical;
+ var offsetAngle = OffsetAngle;
+ var layoutChildren = Children.Where(element => element.IsVisible).ToList();
+
+ if (layoutChildren.Count == 0)
+ {
+ return finalSize;
+ }
+
+ var i = 0;
+ var perDeg = 360.0 / layoutChildren.Count;
+ var radius = Diameter / 2;
+
+ foreach (var element in layoutChildren)
+ {
+ var centerX = element.DesiredSize.Width / 2.0;
+ var centerY = element.DesiredSize.Height / 2.0;
+ var angle = perDeg * i++ + offsetAngle;
+
+ element.RenderTransformOrigin = new RelativePoint(centerX, centerY, RelativeUnit.Absolute);
+ element.RenderTransform = new RotateTransform
+ {
+ Angle = keepVertical ? 0 : angle
+ };
+
+ var r = Math.PI * angle / 180.0;
+ var x = radius * Math.Cos(r);
+ var y = radius * Math.Sin(r);
+
+ var rectX = x + finalSize.Width / 2 - centerX;
+ var rectY = y + finalSize.Height / 2 - centerY;
+
+ element.Arrange(new Rect(rectX, rectY, element.DesiredSize.Width, element.DesiredSize.Height));
+ }
+
+ return finalSize;
+ }
+}