From 22cbc471568c96e18d38b0985b0412571ecb1dce Mon Sep 17 00:00:00 2001
From: NaBian <836904362@qq.com>
Date: Sat, 13 Jun 2026 15:12:10 +0800
Subject: [PATCH] feat: Add WaterfallPanel control and demo.
Introduce a new WaterfallPanel control for Avalonia and a demo UserControl showcasing its usage. The panel supports Groups, AutoGroup, DesiredLength (coerced to >= 0) and Orientation; AutoGroup computes columns/rows based on DesiredLength. Measure/Arrange logic distributes children into the shortest group (waterfall layout) and sizes items by dividing the primary axis by group count. Also add WaterfallPanelDemo.axaml and code-behind demonstrating horizontal/vertical examples with a NumericUpDown to adjust groups.
---
.../Styles/WaterfallPanelDemo.axaml | 132 +++++++++++++++
.../Styles/WaterfallPanelDemo.axaml.cs | 9 +
.../Controls/Panel/WaterfallPanel.cs | 158 ++++++++++++++++++
3 files changed, 299 insertions(+)
create mode 100644 src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml
create mode 100644 src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml.cs
create mode 100644 src/Avalonia/HandyControl_Avalonia/Controls/Panel/WaterfallPanel.cs
diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml
new file mode 100644
index 00000000..b1a5fe62
--- /dev/null
+++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml.cs b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml.cs
new file mode 100644
index 00000000..f4f7cbba
--- /dev/null
+++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml.cs
@@ -0,0 +1,9 @@
+namespace HandyControlDemo.UserControl;
+
+public partial class WaterfallPanelDemo : Avalonia.Controls.UserControl
+{
+ public WaterfallPanelDemo()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/Avalonia/HandyControl_Avalonia/Controls/Panel/WaterfallPanel.cs b/src/Avalonia/HandyControl_Avalonia/Controls/Panel/WaterfallPanel.cs
new file mode 100644
index 00000000..c689a9cd
--- /dev/null
+++ b/src/Avalonia/HandyControl_Avalonia/Controls/Panel/WaterfallPanel.cs
@@ -0,0 +1,158 @@
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using HandyControl.Data;
+using HandyControl.Tools;
+
+namespace HandyControl.Controls;
+
+public class WaterfallPanel : Panel
+{
+ public static readonly StyledProperty GroupsProperty =
+ AvaloniaProperty.Register(nameof(Groups), defaultValue: 2,
+ validate: IsGroupsValid);
+
+ public int Groups
+ {
+ get => GetValue(GroupsProperty);
+ set => SetValue(GroupsProperty, value);
+ }
+
+ private static bool IsGroupsValid(int value) => value >= 1;
+
+ public static readonly StyledProperty AutoGroupProperty =
+ AvaloniaProperty.Register(nameof(AutoGroup));
+
+ public bool AutoGroup
+ {
+ get => GetValue(AutoGroupProperty);
+ set => SetValue(AutoGroupProperty, value);
+ }
+
+ public static readonly StyledProperty DesiredLengthProperty =
+ AvaloniaProperty.Register(nameof(DesiredLength), defaultValue: 0d,
+ coerce: CoerceLength);
+
+ public double DesiredLength
+ {
+ get => GetValue(DesiredLengthProperty);
+ set => SetValue(DesiredLengthProperty, value);
+ }
+
+ public static readonly StyledProperty OrientationProperty =
+ StackPanel.OrientationProperty.AddOwner(new StyledPropertyMetadata(
+ defaultValue: Orientation.Horizontal));
+
+ public Orientation Orientation
+ {
+ get => GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ static WaterfallPanel()
+ {
+ AffectsMeasure(
+ GroupsProperty,
+ AutoGroupProperty,
+ DesiredLengthProperty,
+ OrientationProperty
+ );
+ }
+
+ private static double CoerceLength(AvaloniaObject _, double length) => length < 0 ? 0 : length;
+
+ private int CalculateGroupCount(PanelUvSize size)
+ {
+ if (!AutoGroup)
+ {
+ return Groups;
+ }
+
+ var itemLength = DesiredLength;
+
+ if (MathHelper.IsVerySmall(itemLength))
+ {
+ return Groups;
+ }
+
+ return (int) (size.U / itemLength);
+ }
+
+ protected override Size MeasureOverride(Size constraint)
+ {
+ var orientation = Orientation;
+ var uvConstraint = new PanelUvSize(orientation, constraint);
+
+ var groups = CalculateGroupCount(uvConstraint);
+ if (groups < 1)
+ {
+ return constraint;
+ }
+
+ var vArr = new double[groups].ToList();
+ var itemU = uvConstraint.U / groups;
+ if (double.IsNaN(itemU) || double.IsInfinity(itemU))
+ {
+ return constraint;
+ }
+
+ var children = Children;
+ for (int i = 0, count = children.Count; i < count; i++)
+ {
+ var child = children[i];
+ if (child == null)
+ {
+ continue;
+ }
+
+ child.Measure(constraint);
+
+ var sz = new PanelUvSize(orientation, child.DesiredSize);
+ var minIndex = vArr.IndexOf(vArr.Min());
+ var minV = vArr[minIndex];
+
+ vArr[minIndex] = minV + sz.V;
+ }
+
+ uvConstraint = new PanelUvSize(orientation, new Size(uvConstraint.ScreenSize.Width, vArr.Max()));
+
+ return uvConstraint.ScreenSize;
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ var orientation = Orientation;
+ var uvConstraint = new PanelUvSize(orientation, finalSize);
+
+ var groups = CalculateGroupCount(uvConstraint);
+ if (groups < 1)
+ {
+ return finalSize;
+ }
+
+ var vArr = new double[groups].ToList();
+ var itemU = uvConstraint.U / groups;
+
+ var children = Children;
+ for (int i = 0, count = children.Count; i < count; i++)
+ {
+ var child = children[i];
+ if (child == null)
+ {
+ continue;
+ }
+
+ var minIndex = vArr.IndexOf(vArr.Min());
+ var minV = vArr[minIndex];
+ var childUvSize = new PanelUvSize(orientation, child.DesiredSize);
+ var childSize = new PanelUvSize(orientation, itemU, childUvSize.V);
+ var childRectSize = new PanelUvSize(orientation, minIndex * itemU, minV);
+
+ child.Arrange(new Rect(new Point(childRectSize.U, childRectSize.V), childSize.ScreenSize));
+ vArr[minIndex] = minV + childUvSize.V;
+ }
+
+ return finalSize;
+ }
+}