committed by
GitHub
15 changed files with 819 additions and 68 deletions
@ -0,0 +1,45 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
|||
|
|||
using System; |
|||
|
|||
namespace Avalonia.Layout |
|||
{ |
|||
internal class LayoutContextAdapter : VirtualizingLayoutContext |
|||
{ |
|||
private readonly NonVirtualizingLayoutContext _nonVirtualizingContext; |
|||
|
|||
public LayoutContextAdapter(NonVirtualizingLayoutContext nonVirtualizingContext) |
|||
{ |
|||
_nonVirtualizingContext = nonVirtualizingContext; |
|||
} |
|||
|
|||
protected override object LayoutStateCore |
|||
{ |
|||
get => _nonVirtualizingContext.LayoutState; |
|||
set => _nonVirtualizingContext.LayoutState = value; |
|||
} |
|||
|
|||
protected override Point LayoutOriginCore |
|||
{ |
|||
get => default; |
|||
set |
|||
{ |
|||
if (value != default) |
|||
{ |
|||
throw new InvalidOperationException("LayoutOrigin must be at (0,0) when RealizationRect is infinite sized."); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected override Rect RealizationRectCore() => new Rect(Size.Infinity); |
|||
|
|||
protected override int ItemCountCore() => _nonVirtualizingContext.Children.Count; |
|||
protected override object GetItemAtCore(int index) => _nonVirtualizingContext.Children[index]; |
|||
protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) => |
|||
_nonVirtualizingContext.Children[index]; |
|||
protected override void RecycleElementCore(ILayoutable element) { } |
|||
} |
|||
} |
|||
@ -0,0 +1,160 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.Layout |
|||
{ |
|||
public class NonVirtualizingStackLayout : NonVirtualizingLayout |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the <see cref="Orientation"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<Orientation> OrientationProperty = |
|||
StackLayout.OrientationProperty.AddOwner<NonVirtualizingStackLayout>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Spacing"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<double> SpacingProperty = |
|||
StackLayout.SpacingProperty.AddOwner<NonVirtualizingStackLayout>(); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the axis along which items are laid out.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// One of the enumeration values that specifies the axis along which items are laid out.
|
|||
/// The default is Vertical.
|
|||
/// </value>
|
|||
public Orientation Orientation |
|||
{ |
|||
get => GetValue(OrientationProperty); |
|||
set => SetValue(OrientationProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the
|
|||
/// direction of the StackLayout's Orientation.
|
|||
/// </summary>
|
|||
public double Spacing |
|||
{ |
|||
get => GetValue(SpacingProperty); |
|||
set => SetValue(SpacingProperty, value); |
|||
} |
|||
|
|||
protected internal override Size MeasureOverride( |
|||
NonVirtualizingLayoutContext context, |
|||
Size availableSize) |
|||
{ |
|||
var extentU = 0.0; |
|||
var extentV = 0.0; |
|||
var childCount = context.Children.Count; |
|||
var isVertical = Orientation == Orientation.Vertical; |
|||
var spacing = Spacing; |
|||
var constraint = isVertical ? |
|||
availableSize.WithHeight(double.PositiveInfinity) : |
|||
availableSize.WithWidth(double.PositiveInfinity); |
|||
|
|||
for (var i = 0; i < childCount; ++i) |
|||
{ |
|||
var element = context.Children[i]; |
|||
|
|||
if (!element.IsVisible) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
element.Measure(constraint); |
|||
|
|||
if (isVertical) |
|||
{ |
|||
extentU += element.DesiredSize.Height; |
|||
extentV = Math.Max(extentV, element.DesiredSize.Width); |
|||
} |
|||
else |
|||
{ |
|||
extentU += element.DesiredSize.Width; |
|||
extentV = Math.Max(extentV, element.DesiredSize.Height); |
|||
} |
|||
|
|||
if (i < childCount - 1) |
|||
{ |
|||
extentU += spacing; |
|||
} |
|||
} |
|||
|
|||
return isVertical ? new Size(extentV, extentU) : new Size(extentU, extentV); |
|||
} |
|||
|
|||
protected internal override Size ArrangeOverride( |
|||
NonVirtualizingLayoutContext context, |
|||
Size finalSize) |
|||
{ |
|||
var u = 0.0; |
|||
var childCount = context.Children.Count; |
|||
var isVertical = Orientation == Orientation.Vertical; |
|||
var spacing = Spacing; |
|||
var bounds = new Rect(); |
|||
|
|||
for (var i = 0; i < childCount; ++i) |
|||
{ |
|||
var element = context.Children[i]; |
|||
|
|||
if (!element.IsVisible) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
bounds = isVertical ? |
|||
LayoutVertical(element, u, finalSize) : |
|||
LayoutHorizontal(element, u, finalSize); |
|||
element.Arrange(bounds); |
|||
u = (isVertical ? bounds.Bottom : bounds.Right) + spacing; |
|||
} |
|||
|
|||
return new Size(bounds.Right, bounds.Bottom); |
|||
} |
|||
|
|||
private static Rect LayoutVertical(ILayoutable element, double y, Size constraint) |
|||
{ |
|||
var x = 0.0; |
|||
var width = element.DesiredSize.Width; |
|||
|
|||
switch (element.HorizontalAlignment) |
|||
{ |
|||
case HorizontalAlignment.Center: |
|||
x += (constraint.Width - element.DesiredSize.Width) / 2; |
|||
break; |
|||
case HorizontalAlignment.Right: |
|||
x += constraint.Width - element.DesiredSize.Width; |
|||
break; |
|||
case HorizontalAlignment.Stretch: |
|||
width = constraint.Width; |
|||
break; |
|||
} |
|||
|
|||
return new Rect(x, y, width, element.DesiredSize.Height); |
|||
} |
|||
|
|||
private static Rect LayoutHorizontal(ILayoutable element, double x, Size constraint) |
|||
{ |
|||
var y = 0.0; |
|||
var height = element.DesiredSize.Height; |
|||
|
|||
switch (element.VerticalAlignment) |
|||
{ |
|||
case VerticalAlignment.Center: |
|||
y += (constraint.Height - element.DesiredSize.Height) / 2; |
|||
break; |
|||
case VerticalAlignment.Bottom: |
|||
y += constraint.Height - element.DesiredSize.Height; |
|||
break; |
|||
case VerticalAlignment.Stretch: |
|||
height = constraint.Height; |
|||
break; |
|||
} |
|||
|
|||
return new Rect(x, y, element.DesiredSize.Width, height); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Layout |
|||
{ |
|||
public class VirtualLayoutContextAdapter : NonVirtualizingLayoutContext |
|||
{ |
|||
private readonly VirtualizingLayoutContext _virtualizingContext; |
|||
private ChildrenCollection _children; |
|||
|
|||
public VirtualLayoutContextAdapter(VirtualizingLayoutContext virtualizingContext) |
|||
{ |
|||
_virtualizingContext = virtualizingContext; |
|||
} |
|||
|
|||
protected override object LayoutStateCore |
|||
{ |
|||
get => _virtualizingContext.LayoutState; |
|||
set => _virtualizingContext.LayoutState = value; |
|||
} |
|||
|
|||
protected override IReadOnlyList<ILayoutable> ChildrenCore => |
|||
_children ?? (_children = new ChildrenCollection(_virtualizingContext)); |
|||
|
|||
private class ChildrenCollection : IReadOnlyList<ILayoutable> |
|||
{ |
|||
private readonly VirtualizingLayoutContext _context; |
|||
public ChildrenCollection(VirtualizingLayoutContext context) => _context = context; |
|||
public ILayoutable this[int index] => _context.GetOrCreateElementAt(index); |
|||
public int Count => _context.ItemCount; |
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
|
|||
public IEnumerator<ILayoutable> GetEnumerator() |
|||
{ |
|||
for (var i = 0; i < Count; ++i) |
|||
{ |
|||
yield return this[i]; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,335 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Layout.UnitTests |
|||
{ |
|||
public class NonVirtualizingStackLayoutTests |
|||
{ |
|||
[Fact] |
|||
public void Lays_Out_Children_Vertically() |
|||
{ |
|||
var target = new NonVirtualizingStackLayout { Orientation = Orientation.Vertical }; |
|||
var context = CreateContext(new[] |
|||
{ |
|||
new Border { Height = 20, Width = 120 }, |
|||
new Border { Height = 30 }, |
|||
new Border { Height = 50 }, |
|||
}); |
|||
|
|||
var desiredSize = target.Measure(context, Size.Infinity); |
|||
var arrangeSize = target.Arrange(context, desiredSize); |
|||
|
|||
Assert.Equal(new Size(120, 100), desiredSize); |
|||
Assert.Equal(new Size(120, 100), arrangeSize); |
|||
Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds); |
|||
Assert.Equal(new Rect(0, 20, 120, 30), context.Children[1].Bounds); |
|||
Assert.Equal(new Rect(0, 50, 120, 50), context.Children[2].Bounds); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Lays_Out_Children_Horizontally() |
|||
{ |
|||
var target = new NonVirtualizingStackLayout { Orientation = Orientation.Horizontal }; |
|||
var context = CreateContext(new[] |
|||
{ |
|||
new Border { Width = 20, Height = 120 }, |
|||
new Border { Width = 30 }, |
|||
new Border { Width = 50 }, |
|||
}); |
|||
|
|||
var desiredSize = target.Measure(context, Size.Infinity); |
|||
var arrangeSize = target.Arrange(context, desiredSize); |
|||
|
|||
Assert.Equal(new Size(100, 120), desiredSize); |
|||
Assert.Equal(new Size(100, 120), arrangeSize); |
|||
Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds); |
|||
Assert.Equal(new Rect(20, 0, 30, 120), context.Children[1].Bounds); |
|||
Assert.Equal(new Rect(50, 0, 50, 120), context.Children[2].Bounds); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Lays_Out_Children_Vertically_With_Spacing() |
|||
{ |
|||
var target = new NonVirtualizingStackLayout |
|||
{ |
|||
Orientation = Orientation.Vertical, |
|||
Spacing = 10, |
|||
}; |
|||
|
|||
var context = CreateContext(new[] |
|||
{ |
|||
new Border { Height = 20, Width = 120 }, |
|||
new Border { Height = 30 }, |
|||
new Border { Height = 50 }, |
|||
}); |
|||
|
|||
var desiredSize = target.Measure(context, Size.Infinity); |
|||
var arrangeSize = target.Arrange(context, desiredSize); |
|||
|
|||
Assert.Equal(new Size(120, 120), desiredSize); |
|||
Assert.Equal(new Size(120, 120), arrangeSize); |
|||
Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds); |
|||
Assert.Equal(new Rect(0, 30, 120, 30), context.Children[1].Bounds); |
|||
Assert.Equal(new Rect(0, 70, 120, 50), context.Children[2].Bounds); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Lays_Out_Children_Horizontally_With_Spacing() |
|||
{ |
|||
var target = new NonVirtualizingStackLayout |
|||
{ |
|||
Orientation = Orientation.Horizontal, |
|||
Spacing = 10, |
|||
}; |
|||
|
|||
var context = CreateContext(new[] |
|||
{ |
|||
new Border { Width = 20, Height = 120 }, |
|||
new Border { Width = 30 }, |
|||
new Border { Width = 50 }, |
|||
}); |
|||
|
|||
var desiredSize = target.Measure(context, Size.Infinity); |
|||
var arrangeSize = target.Arrange(context, desiredSize); |
|||
|
|||
Assert.Equal(new Size(120, 120), desiredSize); |
|||
Assert.Equal(new Size(120, 120), arrangeSize); |
|||
Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds); |
|||
Assert.Equal(new Rect(30, 0, 30, 120), context.Children[1].Bounds); |
|||
Assert.Equal(new Rect(70, 0, 50, 120), context.Children[2].Bounds); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Arranges_Vertical_Children_With_Correct_Bounds() |
|||
{ |
|||
var target = new NonVirtualizingStackLayout |
|||
{ |
|||
Orientation = Orientation.Vertical |
|||
}; |
|||
|
|||
var context = CreateContext(new[] |
|||
{ |
|||
new TestControl |
|||
{ |
|||
HorizontalAlignment = HorizontalAlignment.Left, |
|||
MeasureSize = new Size(50, 10), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
HorizontalAlignment = HorizontalAlignment.Left, |
|||
MeasureSize = new Size(150, 10), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
MeasureSize = new Size(50, 10), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
MeasureSize = new Size(150, 10), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
HorizontalAlignment = HorizontalAlignment.Right, |
|||
MeasureSize = new Size(50, 10), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
HorizontalAlignment = HorizontalAlignment.Right, |
|||
MeasureSize = new Size(150, 10), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
HorizontalAlignment = HorizontalAlignment.Stretch, |
|||
MeasureSize = new Size(50, 10), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
HorizontalAlignment = HorizontalAlignment.Stretch, |
|||
MeasureSize = new Size(150, 10), |
|||
}, |
|||
}); |
|||
|
|||
var desiredSize = target.Measure(context, new Size(100, 150)); |
|||
Assert.Equal(new Size(100, 80), desiredSize); |
|||
|
|||
target.Arrange(context, desiredSize); |
|||
|
|||
var bounds = context.Children.Select(x => x.Bounds).ToArray(); |
|||
|
|||
Assert.Equal( |
|||
new[] |
|||
{ |
|||
new Rect(0, 0, 50, 10), |
|||
new Rect(0, 10, 100, 10), |
|||
new Rect(25, 20, 50, 10), |
|||
new Rect(0, 30, 100, 10), |
|||
new Rect(50, 40, 50, 10), |
|||
new Rect(0, 50, 100, 10), |
|||
new Rect(0, 60, 100, 10), |
|||
new Rect(0, 70, 100, 10), |
|||
|
|||
}, bounds); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Arranges_Horizontal_Children_With_Correct_Bounds() |
|||
{ |
|||
var target = new NonVirtualizingStackLayout |
|||
{ |
|||
Orientation = Orientation.Horizontal |
|||
}; |
|||
|
|||
var context = CreateContext(new[] |
|||
{ |
|||
new TestControl |
|||
{ |
|||
VerticalAlignment = VerticalAlignment.Top, |
|||
MeasureSize = new Size(10, 50), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
VerticalAlignment = VerticalAlignment.Top, |
|||
MeasureSize = new Size(10, 150), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
VerticalAlignment = VerticalAlignment.Center, |
|||
MeasureSize = new Size(10, 50), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
VerticalAlignment = VerticalAlignment.Center, |
|||
MeasureSize = new Size(10, 150), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
VerticalAlignment = VerticalAlignment.Bottom, |
|||
MeasureSize = new Size(10, 50), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
VerticalAlignment = VerticalAlignment.Bottom, |
|||
MeasureSize = new Size(10, 150), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
VerticalAlignment = VerticalAlignment.Stretch, |
|||
MeasureSize = new Size(10, 50), |
|||
}, |
|||
new TestControl |
|||
{ |
|||
VerticalAlignment = VerticalAlignment.Stretch, |
|||
MeasureSize = new Size(10, 150), |
|||
}, |
|||
}); |
|||
|
|||
var desiredSize = target.Measure(context, new Size(150, 100)); |
|||
Assert.Equal(new Size(80, 100), desiredSize); |
|||
|
|||
target.Arrange(context, desiredSize); |
|||
|
|||
var bounds = context.Children.Select(x => x.Bounds).ToArray(); |
|||
|
|||
Assert.Equal( |
|||
new[] |
|||
{ |
|||
new Rect(0, 0, 10, 50), |
|||
new Rect(10, 0, 10, 100), |
|||
new Rect(20, 25, 10, 50), |
|||
new Rect(30, 0, 10, 100), |
|||
new Rect(40, 50, 10, 50), |
|||
new Rect(50, 0, 10, 100), |
|||
new Rect(60, 0, 10, 100), |
|||
new Rect(70, 0, 10, 100), |
|||
}, bounds); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(Orientation.Horizontal)] |
|||
[InlineData(Orientation.Vertical)] |
|||
public void Spacing_Not_Added_For_Invisible_Children(Orientation orientation) |
|||
{ |
|||
var targetThreeChildrenOneInvisble = new NonVirtualizingStackLayout |
|||
{ |
|||
Orientation = orientation, |
|||
Spacing = 40, |
|||
}; |
|||
|
|||
var contextThreeChildrenOneInvisble = CreateContext(new[] |
|||
{ |
|||
new StackPanel { Width = 10, Height= 10, IsVisible = false }, |
|||
new StackPanel { Width = 10, Height= 10 }, |
|||
new StackPanel { Width = 10, Height= 10 }, |
|||
}); |
|||
|
|||
var targetTwoChildrenNoneInvisible = new NonVirtualizingStackLayout |
|||
{ |
|||
Spacing = 40, |
|||
Orientation = orientation, |
|||
}; |
|||
|
|||
var contextTwoChildrenNoneInvisible = CreateContext(new[] |
|||
{ |
|||
new StackPanel { Width = 10, Height = 10 }, |
|||
new StackPanel { Width = 10, Height = 10 } |
|||
}); |
|||
|
|||
var desiredSize1 = targetThreeChildrenOneInvisble.Measure(contextThreeChildrenOneInvisble, Size.Infinity); |
|||
var desiredSize2 = targetTwoChildrenNoneInvisible.Measure(contextTwoChildrenNoneInvisible, Size.Infinity); |
|||
|
|||
Assert.Equal(desiredSize2, desiredSize1); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(Orientation.Horizontal)] |
|||
[InlineData(Orientation.Vertical)] |
|||
public void Only_Arrange_Visible_Children(Orientation orientation) |
|||
{ |
|||
var hiddenPanel = new Panel { Width = 10, Height = 10, IsVisible = false }; |
|||
var panel = new Panel { Width = 10, Height = 10 }; |
|||
|
|||
var target = new NonVirtualizingStackLayout |
|||
{ |
|||
Spacing = 40, |
|||
Orientation = orientation, |
|||
}; |
|||
|
|||
var context = CreateContext(new[] |
|||
{ |
|||
hiddenPanel, |
|||
panel |
|||
}); |
|||
|
|||
var desiredSize = target.Measure(context, Size.Infinity); |
|||
var arrangeSize = target.Arrange(context, desiredSize); |
|||
Assert.Equal(new Size(10, 10), arrangeSize); |
|||
} |
|||
|
|||
private NonVirtualizingLayoutContext CreateContext(Control[] children) |
|||
{ |
|||
return new TestLayoutContext(children); |
|||
} |
|||
|
|||
private class TestLayoutContext : NonVirtualizingLayoutContext |
|||
{ |
|||
public TestLayoutContext(Control[] children) => ChildrenCore = children; |
|||
protected override IReadOnlyList<ILayoutable> ChildrenCore { get; } |
|||
} |
|||
|
|||
private class TestControl : Control |
|||
{ |
|||
public Size MeasureConstraint { get; private set; } |
|||
public Size MeasureSize { get; set; } |
|||
|
|||
protected override Size MeasureOverride(Size availableSize) |
|||
{ |
|||
MeasureConstraint = availableSize; |
|||
return MeasureSize; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue