diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
index e600e644af..08bb1584a9 100644
--- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
@@ -12,6 +12,8 @@
Stack - Horizontal
UniformGrid - Vertical
UniformGrid - Horizontal
+ WrapLayout - Horizontal
+ WrapLayout - Veritcal
@@ -24,12 +26,16 @@
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
+
+
+
-
+
+
+
diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
index cce80a2d3c..a4003cadc9 100644
--- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
@@ -79,6 +79,26 @@ namespace ControlCatalog.Pages
MinItemHeight = 200,
};
break;
+ case 4:
+ _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ _repeater.Layout = new WrapLayout
+ {
+ Orientation = Orientation.Vertical,
+ HorizontalSpacing = 20,
+ VerticalSpacing = 20
+ };
+ break;
+ case 5:
+ _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _repeater.Layout = new WrapLayout
+ {
+ Orientation = Orientation.Horizontal,
+ HorizontalSpacing = 20,
+ VerticalSpacing = 20
+ };
+ break;
}
}
diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs
index 133d9d4baa..5bd46b6714 100644
--- a/src/Avalonia.Headless/HeadlessWindowImpl.cs
+++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs
@@ -244,6 +244,8 @@ namespace Avalonia.Headless
public Action LostFocus { get; set; }
+ public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
+
void IHeadlessWindow.KeyPress(Key key, RawInputModifiers modifiers)
{
Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyDown, key, modifiers));
diff --git a/src/Avalonia.Layout/WrapLayout/UvBounds.cs b/src/Avalonia.Layout/WrapLayout/UvBounds.cs
new file mode 100644
index 0000000000..1c69788732
--- /dev/null
+++ b/src/Avalonia.Layout/WrapLayout/UvBounds.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+using Avalonia;
+using Avalonia.Layout;
+
+namespace Avalonia.Layout
+{
+ internal struct UvBounds
+ {
+ public UvBounds(Orientation orientation, Rect rect)
+ {
+ if (orientation == Orientation.Horizontal)
+ {
+ UMin = rect.Left;
+ UMax = rect.Right;
+ VMin = rect.Top;
+ VMax = rect.Bottom;
+ }
+ else
+ {
+ UMin = rect.Top;
+ UMax = rect.Bottom;
+ VMin = rect.Left;
+ VMax = rect.Right;
+ }
+ }
+
+ public double UMin { get; }
+
+ public double UMax { get; }
+
+ public double VMin { get; }
+
+ public double VMax { get; }
+ }
+}
diff --git a/src/Avalonia.Layout/WrapLayout/UvMeasure.cs b/src/Avalonia.Layout/WrapLayout/UvMeasure.cs
new file mode 100644
index 0000000000..e10e6bd0f8
--- /dev/null
+++ b/src/Avalonia.Layout/WrapLayout/UvMeasure.cs
@@ -0,0 +1,45 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+using Avalonia.Layout;
+
+namespace Avalonia.Layout
+{
+ internal struct UvMeasure
+ {
+ internal static readonly UvMeasure Zero = default(UvMeasure);
+
+ internal double U { get; set; }
+
+ internal double V { get; set; }
+
+ public UvMeasure(Orientation orientation, double width, double height)
+ {
+ if (orientation == Orientation.Horizontal)
+ {
+ U = width;
+ V = height;
+ }
+ else
+ {
+ U = height;
+ V = width;
+ }
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is UvMeasure measure)
+ {
+ return (measure.U == U) && (measure.V == V);
+ }
+
+ return false;
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/WrapLayout/WrapItem.cs b/src/Avalonia.Layout/WrapLayout/WrapItem.cs
new file mode 100644
index 0000000000..2f0b39a2d1
--- /dev/null
+++ b/src/Avalonia.Layout/WrapLayout/WrapItem.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Avalonia.Controls;
+using Avalonia.Layout;
+
+namespace Avalonia.Layout
+{
+ internal class WrapItem
+ {
+ public WrapItem(int index)
+ {
+ this.Index = index;
+ }
+
+ public int Index { get; }
+
+ public UvMeasure? Measure { get; internal set; }
+
+ public UvMeasure? Position { get; internal set; }
+
+ public ILayoutable Element { get; internal set; }
+ }
+}
diff --git a/src/Avalonia.Layout/WrapLayout/WrapLayout.cs b/src/Avalonia.Layout/WrapLayout/WrapLayout.cs
new file mode 100644
index 0000000000..dccbb2205e
--- /dev/null
+++ b/src/Avalonia.Layout/WrapLayout/WrapLayout.cs
@@ -0,0 +1,327 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Layout;
+using System;
+using System.Collections.Specialized;
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Arranges elements by wrapping them to fit the available space.
+ /// When is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row.
+ /// When is set to Orientation.Vertical, element are arranged in columns until the available height is reached.
+ ///
+ public class WrapLayout : VirtualizingLayout
+ {
+ ///
+ /// Gets or sets a uniform Horizontal distance (in pixels) between items when is set to Horizontal,
+ /// or between columns of items when is set to Vertical.
+ ///
+ public double HorizontalSpacing
+ {
+ get { return (double)GetValue(HorizontalSpacingProperty); }
+ set { SetValue(HorizontalSpacingProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly StyledProperty HorizontalSpacingProperty =
+ AvaloniaProperty.Register(nameof(HorizontalSpacing), 0);
+
+ ///
+ /// Gets or sets a uniform Vertical distance (in pixels) between items when is set to Vertical,
+ /// or between rows of items when is set to Horizontal.
+ ///
+ public double VerticalSpacing
+ {
+ get { return (double)GetValue(VerticalSpacingProperty); }
+ set { SetValue(VerticalSpacingProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly StyledProperty VerticalSpacingProperty =
+ AvaloniaProperty.Register(
+ nameof(VerticalSpacing), 0d);
+
+ ///
+ /// Gets or sets the orientation of the WrapLayout.
+ /// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls.
+ /// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added.
+ ///
+ public Orientation Orientation
+ {
+ get { return (Orientation)GetValue(OrientationProperty); }
+ set { SetValue(OrientationProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly StyledProperty OrientationProperty =
+ AvaloniaProperty.Register(
+ nameof(Orientation),
+ Orientation.Horizontal);
+
+ ///
+ protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ var state = new WrapLayoutState(context);
+ context.LayoutState = state;
+ base.InitializeForContextCore(context);
+ }
+
+ ///
+ protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ context.LayoutState = null;
+ base.UninitializeForContextCore(context);
+ }
+
+ ///
+ protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
+ {
+ var state = (WrapLayoutState)context.LayoutState;
+
+ switch (args.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ state.RemoveFromIndex(args.NewStartingIndex);
+ break;
+ case NotifyCollectionChangedAction.Move:
+ int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex);
+ state.RemoveFromIndex(minIndex);
+
+ state.RecycleElementAt(args.OldStartingIndex);
+ state.RecycleElementAt(args.NewStartingIndex);
+ break;
+ case NotifyCollectionChangedAction.Remove:
+ state.RemoveFromIndex(args.OldStartingIndex);
+ break;
+ case NotifyCollectionChangedAction.Replace:
+ state.RemoveFromIndex(args.NewStartingIndex);
+ state.RecycleElementAt(args.NewStartingIndex);
+ break;
+ case NotifyCollectionChangedAction.Reset:
+ state.Clear();
+ break;
+ }
+
+ base.OnItemsChangedCore(context, source, args);
+ }
+
+ ///
+ protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
+ {
+ var totalMeasure = UvMeasure.Zero;
+ var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height);
+ var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing);
+ var realizationBounds = new UvBounds(Orientation, context.RealizationRect);
+ var position = UvMeasure.Zero;
+
+ var state = (WrapLayoutState)context.LayoutState;
+ if (state.Orientation != Orientation)
+ {
+ state.SetOrientation(Orientation);
+ }
+
+ if (spacingMeasure.Equals(state.Spacing) == false)
+ {
+ state.ClearPositions();
+ state.Spacing = spacingMeasure;
+ }
+
+ if (state.AvailableU != parentMeasure.U)
+ {
+ state.ClearPositions();
+ state.AvailableU = parentMeasure.U;
+ }
+
+ double currentV = 0;
+ for (int i = 0; i < context.ItemCount; i++)
+ {
+ bool measured = false;
+ WrapItem item = state.GetItemAt(i);
+ if (item.Measure == null)
+ {
+ item.Element = context.GetOrCreateElementAt(i);
+ item.Element.Measure(availableSize);
+ item.Measure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height);
+ measured = true;
+ }
+
+ UvMeasure currentMeasure = item.Measure.Value;
+ if (currentMeasure.U == 0)
+ {
+ continue; // ignore collapsed items
+ }
+
+ if (item.Position == null)
+ {
+ if (parentMeasure.U < position.U + currentMeasure.U)
+ {
+ // New Row
+ position.U = 0;
+ position.V += currentV + spacingMeasure.V;
+ currentV = 0;
+ }
+
+ item.Position = position;
+ }
+
+ position = item.Position.Value;
+
+ double vEnd = position.V + currentMeasure.V;
+ if (vEnd < realizationBounds.VMin)
+ {
+ // Item is "above" the bounds
+ if (item.Element != null)
+ {
+ context.RecycleElement(item.Element);
+ item.Element = null;
+ }
+ }
+ else if (position.V > realizationBounds.VMax)
+ {
+ // Item is "below" the bounds.
+ if (item.Element != null)
+ {
+ context.RecycleElement(item.Element);
+ item.Element = null;
+ }
+
+ // We don't need to measure anything below the bounds
+ break;
+ }
+ else if (measured == false)
+ {
+ // Always measure elements that are within the bounds
+ item.Element = context.GetOrCreateElementAt(i);
+ item.Element.Measure(availableSize);
+
+ currentMeasure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height);
+ if (currentMeasure.Equals(item.Measure) == false)
+ {
+ // this item changed size; we need to recalculate layout for everything after this
+ state.RemoveFromIndex(i + 1);
+ item.Measure = currentMeasure;
+
+ // did the change make it go into the new row?
+ if (parentMeasure.U < position.U + currentMeasure.U)
+ {
+ // New Row
+ position.U = 0;
+ position.V += currentV + spacingMeasure.V;
+ currentV = 0;
+ }
+
+ item.Position = position;
+ }
+ }
+
+ position.U += currentMeasure.U + spacingMeasure.U;
+ currentV = Math.Max(currentMeasure.V, currentV);
+ }
+
+ // update value with the last line
+ // if the the last loop is(parentMeasure.U > currentMeasure.U + lineMeasure.U) the total isn't calculated then calculate it
+ // if the last loop is (parentMeasure.U > currentMeasure.U) the currentMeasure isn't added to the total so add it here
+ // for the last condition it is zeros so adding it will make no difference
+ // this way is faster than an if condition in every loop for checking the last item
+ totalMeasure.U = parentMeasure.U;
+ totalMeasure.V = state.GetHeight();
+
+ totalMeasure.U = Math.Ceiling(totalMeasure.U);
+
+ return Orientation == Orientation.Horizontal ? new Size(totalMeasure.U, totalMeasure.V) : new Size(totalMeasure.V, totalMeasure.U);
+ }
+
+ ///
+ protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
+ {
+ if (context.ItemCount > 0)
+ {
+ var parentMeasure = new UvMeasure(Orientation, finalSize.Width, finalSize.Height);
+ var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing);
+ var realizationBounds = new UvBounds(Orientation, context.RealizationRect);
+
+ var state = (WrapLayoutState)context.LayoutState;
+ bool Arrange(WrapItem item, bool isLast = false)
+ {
+ if (item.Measure.HasValue == false)
+ {
+ return false;
+ }
+
+ if (item.Position == null)
+ {
+ return false;
+ }
+
+ var desiredMeasure = item.Measure.Value;
+ if (desiredMeasure.U == 0)
+ {
+ return true; // if an item is collapsed, avoid adding the spacing
+ }
+
+ UvMeasure position = item.Position.Value;
+
+ // Stretch the last item to fill the available space
+ if (isLast)
+ {
+ desiredMeasure.U = parentMeasure.U - position.U;
+ }
+
+ if (((position.V + desiredMeasure.V) >= realizationBounds.VMin) && (position.V <= realizationBounds.VMax))
+ {
+ // place the item
+ var child = context.GetOrCreateElementAt(item.Index);
+ if (Orientation == Orientation.Horizontal)
+ {
+ child.Arrange(new Rect(position.U, position.V, desiredMeasure.U, desiredMeasure.V));
+ }
+ else
+ {
+ child.Arrange(new Rect(position.V, position.U, desiredMeasure.V, desiredMeasure.U));
+ }
+ }
+ else if (position.V > realizationBounds.VMax)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ for (var i = 0; i < context.ItemCount; i++)
+ {
+ bool continueArranging = Arrange(state.GetItemAt(i));
+ if (continueArranging == false)
+ {
+ break;
+ }
+ }
+ }
+
+ return finalSize;
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == OrientationProperty || change.Property == HorizontalSpacingProperty || change.Property == VerticalSpacingProperty)
+ {
+ InvalidateMeasure();
+ InvalidateArrange();
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/WrapLayout/WrapLayoutState.cs b/src/Avalonia.Layout/WrapLayout/WrapLayoutState.cs
new file mode 100644
index 0000000000..7955ea983c
--- /dev/null
+++ b/src/Avalonia.Layout/WrapLayout/WrapLayoutState.cs
@@ -0,0 +1,143 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Avalonia.Layout
+{
+ internal class WrapLayoutState
+ {
+ private List _items = new List();
+ private VirtualizingLayoutContext _context;
+
+ public WrapLayoutState(VirtualizingLayoutContext context)
+ {
+ this._context = context;
+ }
+
+ public Orientation Orientation { get; private set; }
+
+ public UvMeasure Spacing { get; internal set; }
+
+ public double AvailableU { get; internal set; }
+
+ internal WrapItem GetItemAt(int index)
+ {
+ if (index < 0)
+ {
+ throw new IndexOutOfRangeException();
+ }
+
+ if (index <= (_items.Count - 1))
+ {
+ return _items[index];
+ }
+ else
+ {
+ WrapItem item = new WrapItem(index);
+ _items.Add(item);
+ return item;
+ }
+ }
+
+ internal void Clear()
+ {
+ _items.Clear();
+ }
+
+ internal void RemoveFromIndex(int index)
+ {
+ if (index >= _items.Count)
+ {
+ // Item was added/removed but we haven't realized that far yet
+ return;
+ }
+
+ int numToRemove = _items.Count - index;
+ _items.RemoveRange(index, numToRemove);
+ }
+
+ internal void SetOrientation(Orientation orientation)
+ {
+ foreach (var item in _items.Where(i => i.Measure.HasValue))
+ {
+ UvMeasure measure = item.Measure.Value;
+ double v = measure.V;
+ measure.V = measure.U;
+ measure.U = v;
+ item.Measure = measure;
+ item.Position = null;
+ }
+
+ Orientation = orientation;
+ AvailableU = 0;
+ }
+
+ internal void ClearPositions()
+ {
+ foreach (var item in _items)
+ {
+ item.Position = null;
+ }
+ }
+
+ internal double GetHeight()
+ {
+ if (_items.Count == 0)
+ {
+ return 0;
+ }
+
+ bool calculateAverage = true;
+ if ((_items.Count == _context.ItemCount) && _items[_items.Count - 1].Position.HasValue)
+ {
+ calculateAverage = false;
+ }
+
+ UvMeasure? lastPosition = null;
+ double maxV = 0;
+
+ int itemCount = _items.Count;
+ for (int i = _items.Count - 1; i >= 0; i--)
+ {
+ var item = _items[i];
+ if (item.Position == null)
+ {
+ itemCount--;
+ continue;
+ }
+
+ if (lastPosition != null)
+ {
+ if (lastPosition.Value.V > item.Position.Value.V)
+ {
+ // This is a row above the last item. Exit and calculate the average
+ break;
+ }
+ }
+
+ lastPosition = item.Position;
+ maxV = Math.Max(maxV, item.Measure.Value.V);
+ }
+
+ double totalHeight = lastPosition.Value.V + maxV;
+ if (calculateAverage)
+ {
+ return (totalHeight / itemCount) * _context.ItemCount;
+ }
+ else
+ {
+ return totalHeight;
+ }
+ }
+
+ internal void RecycleElementAt(int index)
+ {
+ var element = _context.GetOrCreateElementAt(index);
+ _context.RecycleElement(element);
+ }
+ }
+}