Merge pull request #759 from irihitech/tag-panel

New Panel: Wrap panel with a trailing item
This commit is contained in:
Dong Bin
2025-09-03 21:37:18 +08:00
committed by GitHub
3 changed files with 315 additions and 2 deletions

View File

@@ -5,8 +5,6 @@ on:
branches: [ "main", "2.0" ]
pull_request:
branches: [ "main", "2.0" ]
pull_request_target:
branches: [ "main", "2.0" ]
workflow_dispatch:
permissions:

View File

@@ -0,0 +1,160 @@
using System.Collections.Specialized;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Irihi.Avalonia.Shared.Helpers;
namespace Ursa.Controls;
public class WrapPanelWithTrailingItem : Panel
{
public static readonly StyledProperty<Layoutable?> TrailingItemProperty =
AvaloniaProperty.Register<WrapPanelWithTrailingItem, Layoutable?>(
nameof(TrailingItem));
public static readonly StyledProperty<double> TrailingWrapWidthProperty =
AvaloniaProperty.Register<WrapPanelWithTrailingItem, double>(
nameof(TrailingWrapWidth));
static WrapPanelWithTrailingItem()
{
AffectsMeasure<WrapPanelWithTrailingItem>(TrailingItemProperty);
AffectsArrange<WrapPanelWithTrailingItem>(TrailingItemProperty);
}
public Layoutable? TrailingItem
{
get => GetValue(TrailingItemProperty);
set => SetValue(TrailingItemProperty, value);
}
public double TrailingWrapWidth
{
get => GetValue(TrailingWrapWidthProperty);
set => SetValue(TrailingWrapWidthProperty, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == TrailingItemProperty)
{
if (change.GetOldValue<Layoutable?>() is { } oldValue)
{
VisualChildren.Remove(oldValue);
if (!IsItemsHost) LogicalChildren.Remove(oldValue);
}
if (change.GetNewValue<Layoutable?>() is { } newValue)
{
VisualChildren.Add(newValue);
if (!IsItemsHost) LogicalChildren.Add(newValue);
}
}
WrapPanel p = new WrapPanel();
}
protected override Size MeasureOverride(Size availableSize)
{
double currentLineX = 0;
double currentLineHeight = 0;
double totalHeight = 0;
var children = Children;
foreach (var child in children)
{
child.Measure(availableSize);
var deltaX = availableSize.Width - currentLineX;
// Width is enough to place next child
if (MathHelpers.GreaterThan(deltaX, child.DesiredSize.Width))
{
currentLineX += child.DesiredSize.Width;
currentLineHeight = Math.Max(currentLineHeight, child.DesiredSize.Height);
}
// Width is not enough to place next child
// reset currentLineX and currentLineHeight
// accumulate last line height to total height.
// Notice: last line height accumulation only happens when restarting a new line, so it needs to finally add one more time outside iteration.
else
{
currentLineX = child.DesiredSize.Width;
totalHeight += currentLineHeight;
currentLineHeight = child.DesiredSize.Height;
}
}
var last = TrailingItem;
if (last is null) return new Size(availableSize.Width, totalHeight);
last.Measure(availableSize);
var lastDeltaX = availableSize.Width - currentLineX;
// If width is not enough, add a new line, and recalculate total height
if (lastDeltaX < TrailingWrapWidth)
{
totalHeight += currentLineHeight;
totalHeight += last.DesiredSize.Height;
}
else
{
currentLineHeight = Math.Max(currentLineHeight, last.DesiredSize.Height);
totalHeight += currentLineHeight;
}
return new Size(availableSize.Width, totalHeight);
}
protected override Size ArrangeOverride(Size finalSize)
{
double currentLineX = 0;
double currentLineHeight = 0;
double totalHeight = 0;
var children = Children;
foreach (var child in children)
{
double deltaX = finalSize.Width - currentLineX;
// Width is enough to place next child
if (MathHelpers.GreaterThan(deltaX, child.DesiredSize.Width))
{
child.Arrange(new Rect(currentLineX, totalHeight, child.DesiredSize.Width,
Math.Max(child.DesiredSize.Height, currentLineHeight)));
currentLineX += child.Bounds.Width;
currentLineHeight = Math.Max(currentLineHeight, child.Bounds.Height);
}
// Width is not enough to place next child
// reset currentLineX and currentLineHeight
// accumulate last line height to total height.
// Notice: last line height accumulation only happens when restarting a new line, so it needs to finally add one more time outside iteration.
else
{
totalHeight += currentLineHeight;
child.Arrange(new Rect(0, totalHeight, Math.Min(child.DesiredSize.Width, finalSize.Width),
child.DesiredSize.Height));
currentLineX = child.Bounds.Width;
currentLineHeight = child.Bounds.Height;
}
}
var last = TrailingItem;
if (last is null) return new Size(finalSize.Width, totalHeight);
var lastDeltaX = finalSize.Width - currentLineX;
// If width is not enough, add a new line, and recalculate total height
if (lastDeltaX < TrailingWrapWidth)
{
totalHeight += currentLineHeight;
last.Arrange(new Rect(0, totalHeight, finalSize.Width, last.DesiredSize.Height));
totalHeight += last.DesiredSize.Height;
}
else
{
currentLineHeight = children.Count == 1 ? finalSize.Height : currentLineHeight;
last.Arrange(new Rect(currentLineX, totalHeight, lastDeltaX,
Math.Max(currentLineHeight, last.DesiredSize.Height)));
currentLineHeight = Math.Max(currentLineHeight, last.DesiredSize.Height);
totalHeight += currentLineHeight;
}
return new Size(finalSize.Width, totalHeight);
}
}

View File

@@ -0,0 +1,155 @@
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Ursa.Controls;
namespace HeadlessTest.Ursa.Controls.PanelTests;
public class WrapPanelWithTrailingItemTests
{
[AvaloniaFact]
public void Visual_Children_Correct()
{
var panel = new WrapPanelWithTrailingItem();
var child1 = new Button { Content = "Button 1"};
var child2 = new Button { Content = "Button 2"};
var trailing = new Button { Content = "Trailing"};
panel.Children.Add(child1);
panel.Children.Add(child2);
panel.TrailingItem = trailing;
var visualChildren = panel.GetVisualChildren().ToList();
Assert.Equal(3, visualChildren.Count);
Assert.Equal(child1, visualChildren[0]);
Assert.Equal(child2, visualChildren[1]);
Assert.Equal(trailing, visualChildren[2]);
var child3 = new Button { Content = "Button 3"};
panel.Children.Add(child3);
visualChildren = panel.GetVisualChildren().ToList();
Assert.Equal(4, visualChildren.Count);
Assert.Equal(child1, visualChildren[0]);
Assert.Equal(child2, visualChildren[1]);
Assert.Equal(child3, visualChildren[2]);
Assert.Equal(trailing, visualChildren[3]);
var trailing2 = new Button { Content = "Trailing2"};
panel.TrailingItem = trailing2;
visualChildren = panel.GetVisualChildren().ToList();
Assert.Equal(4, visualChildren.Count);
Assert.Equal(child1, visualChildren[0]);
Assert.Equal(child2, visualChildren[1]);
Assert.Equal(child3, visualChildren[2]);
Assert.Equal(trailing2, visualChildren[3]);
panel.Children.Remove(child2);
visualChildren = panel.GetVisualChildren().ToList();
Assert.Equal(3, visualChildren.Count);
Assert.Equal(child1, visualChildren[0]);
Assert.Equal(child3, visualChildren[1]);
Assert.Equal(trailing2, visualChildren[2]);
}
[AvaloniaFact]
// Items Appears in Logical Children because IsItemsHost is false for individual Panels
public void Logical_Children_Correct()
{
var panel = new WrapPanelWithTrailingItem();
var child1 = new Button { Content = "Button 1"};
var child2 = new Button { Content = "Button 2"};
var trailing = new Button { Content = "Trailing"};
panel.Children.Add(child1);
panel.Children.Add(child2);
panel.TrailingItem = trailing;
var logicalChildren = panel.GetLogicalChildren().ToList();
Assert.Equal(3, logicalChildren.Count);
Assert.Equal(child1, logicalChildren[0]);
Assert.Equal(child2, logicalChildren[1]);
Assert.Equal(trailing, logicalChildren[2]);
var child3 = new Button { Content = "Button 3"};
panel.Children.Add(child3);
logicalChildren = panel.GetLogicalChildren().ToList();
Assert.Equal(4, logicalChildren.Count);
Assert.Equal(child1, logicalChildren[0]);
Assert.Equal(child2, logicalChildren[1]);
Assert.Equal(child3, logicalChildren[2]);
Assert.Equal(trailing, logicalChildren[3]);
var trailing2 = new Button { Content = "Trailing2"};
panel.TrailingItem = trailing2;
logicalChildren = panel.GetLogicalChildren().ToList();
Assert.Equal(4, logicalChildren.Count);
Assert.Equal(child1, logicalChildren[0]);
Assert.Equal(child2, logicalChildren[1]);
Assert.Equal(child3, logicalChildren[2]);
Assert.Equal(trailing2, logicalChildren[3]);
panel.Children.Remove(child2);
logicalChildren = panel.GetLogicalChildren().ToList();
Assert.Equal(3, logicalChildren.Count);
Assert.Equal(child1, logicalChildren[0]);
Assert.Equal(child3, logicalChildren[1]);
Assert.Equal(trailing2, logicalChildren[2]);
}
[AvaloniaFact]
public void Measure_Arrange_Children()
{
var window = new Window()
{
Height = 1000, Width = 1000, VerticalContentAlignment = VerticalAlignment.Stretch
};
var panel = new WrapPanelWithTrailingItem() { TrailingWrapWidth = 30 };
var child1 = new Button { Content = "Button 1", Width = 200, Height = 100 };
var child2 = new Button { Content = "Button 2", Width = 300, Height = 100 };
var trailing = new Button
{
Content = "Trailing",
Height = 100,
HorizontalAlignment = HorizontalAlignment.Stretch
};
window.Content = panel;
panel.Children.Add(child1);
panel.Children.Add(child2);
panel.TrailingItem = trailing;
window.Show();
Dispatcher.UIThread.RunJobs();
Assert.Equal(200, child1.Bounds.Width);
Assert.Equal(300, child2.Bounds.Width);
Assert.Equal(500, trailing.Bounds.Width);
panel.Width = 600;
Dispatcher.UIThread.RunJobs();
Assert.Equal(200, child1.Bounds.Width);
Assert.Equal(300, child2.Bounds.Width);
Assert.Equal(100, trailing.Bounds.Width);
panel.Width = 510;
Dispatcher.UIThread.RunJobs();
Assert.Equal(200, child1.Bounds.Width);
Assert.Equal(300, child2.Bounds.Width);
Assert.Equal(510, trailing.Bounds.Width);
Assert.Equal(100, trailing.Bounds.Y);
panel.Width = 300;
Dispatcher.UIThread.RunJobs();
Assert.Equal(200, child1.Bounds.Width);
Assert.Equal(300, child2.Bounds.Width);
Assert.Equal(300, trailing.Bounds.Width);
Assert.Equal(200, trailing.Bounds.Y);
}
}