Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: WrapLayout #73

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion components/Primitives/src/WrapLayout/UvBounds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Windows.Foundation;
using Microsoft.UI.Xaml.Controls;

namespace CommunityToolkit.WinUI.Controls;

internal struct UvBounds
{
public UvBounds(Orientation orientation, Rect rect)
{
if (orientation == Orientation.Horizontal)
if (orientation is Orientation.Horizontal)
{
UMin = rect.Left;
UMax = rect.Right;
Expand Down
48 changes: 35 additions & 13 deletions components/Primitives/src/WrapLayout/UvMeasure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,61 @@
// 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.Diagnostics;
using Windows.Foundation;
using Microsoft.UI.Xaml.Controls;

namespace CommunityToolkit.WinUI.Controls;

[DebuggerDisplay("U = {U} V = {V}")]
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)
public UvMeasure(Orientation orientation, Size size)
{
if (orientation == Orientation.Horizontal)
if (orientation is Orientation.Horizontal)
{
U = width;
V = height;
U = size.Width;
V = size.Height;
}
else
{
U = height;
V = width;
U = size.Height;
V = size.Width;
}
}

public Point GetPoint(Orientation orientation)
{
return orientation is Orientation.Horizontal ? new Point(U, V) : new Point(V, U);
}

public Size GetSize(Orientation orientation)
{
return orientation is Orientation.Horizontal ? new Size(U, V) : new Size(V, U);
}

public static bool operator ==(UvMeasure measure1, UvMeasure measure2)
{
return measure1.U == measure2.U && measure1.V == measure2.V;
}

public static bool operator !=(UvMeasure measure1, UvMeasure measure2)
{
return !(measure1 == measure2);
}

public override bool Equals(object? obj)
{
if (obj is UvMeasure measure)
{
return (measure.U == U) && (measure.V == V);
}
return obj is UvMeasure measure && this == measure;
}

return false;
public bool Equals(UvMeasure value)
{
return this == value;
}

public override int GetHashCode()
Expand Down
4 changes: 3 additions & 1 deletion components/Primitives/src/WrapLayout/WrapItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.UI.Xaml;

namespace CommunityToolkit.WinUI.Controls;

internal class WrapItem
{
public WrapItem(int index)
{
this.Index = index;
Index = index;
}

public int Index { get; }
Expand Down
126 changes: 46 additions & 80 deletions components/Primitives/src/WrapLayout/WrapLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
// 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 Microsoft.UI.Xaml.Controls;
using System.Collections.Specialized;
using Windows.Foundation;
using Microsoft.UI.Xaml;

namespace CommunityToolkit.WinUI.Controls;

Expand All @@ -20,8 +23,8 @@ public class WrapLayout : VirtualizingLayout
/// </summary>
public double HorizontalSpacing
{
get { return (double)GetValue(HorizontalSpacingProperty); }
set { SetValue(HorizontalSpacingProperty, value); }
get => (double)GetValue(HorizontalSpacingProperty);
set => SetValue(HorizontalSpacingProperty, value);
}

/// <summary>
Expand All @@ -40,8 +43,8 @@ public double HorizontalSpacing
/// </summary>
public double VerticalSpacing
{
get { return (double)GetValue(VerticalSpacingProperty); }
set { SetValue(VerticalSpacingProperty, value); }
get => (double)GetValue(VerticalSpacingProperty);
set => SetValue(VerticalSpacingProperty, value);
}

/// <summary>
Expand All @@ -61,8 +64,8 @@ public double VerticalSpacing
/// </summary>
public Orientation Orientation
{
get { return (Orientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
get => (Orientation)GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}

/// <summary>
Expand All @@ -87,8 +90,7 @@ private static void LayoutPropertyChanged(DependencyObject d, DependencyProperty
/// <inheritdoc />
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
{
var state = new WrapLayoutState(context);
context.LayoutState = state;
context.LayoutState = new WrapLayoutState(context);
base.InitializeForContextCore(context);
}

Expand All @@ -110,7 +112,7 @@ protected override void OnItemsChangedCore(VirtualizingLayoutContext context, ob
state.RemoveFromIndex(args.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Move:
int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex);
var minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex);
state.RemoveFromIndex(minIndex);

state.RecycleElementAt(args.OldStartingIndex);
Expand All @@ -134,50 +136,40 @@ protected override void OnItemsChangedCore(VirtualizingLayoutContext context, ob
/// <inheritdoc />
protected 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 parentMeasure = new UvMeasure(Orientation, availableSize);
var spacingMeasure = new UvMeasure(Orientation, new Size(HorizontalSpacing, VerticalSpacing));

var state = (WrapLayoutState)context.LayoutState;
if (state.Orientation != Orientation)
{
state.SetOrientation(Orientation);
}

if (spacingMeasure.Equals(state.Spacing) == false)
if (spacingMeasure != state.Spacing || state.AvailableU != parentMeasure.U)
{
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++)
var realizationBounds = new UvBounds(Orientation, context.RealizationRect);
var position = new UvMeasure();
for (var i = 0; i < context.ItemCount; ++i)
{
bool measured = false;
WrapItem item = state.GetItemAt(i);
if (item.Measure == null)
var measured = false;
var item = state.GetItemAt(i);
if (item.Measure is null)
{
item.Element = context.GetOrCreateElementAt(i);
item.Element.Measure(availableSize);
item.Measure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height);
item.Measure = new UvMeasure(Orientation, item.Element.DesiredSize);
measured = true;
}

UvMeasure currentMeasure = item.Measure.Value;
if (currentMeasure.U == 0)
{
continue; // ignore collapsed items
}
var currentMeasure = item.Measure.Value;

if (item.Position == null)
if (item.Position is null)
{
if (parentMeasure.U < position.U + currentMeasure.U)
{
Expand All @@ -192,20 +184,22 @@ protected override Size MeasureOverride(VirtualizingLayoutContext context, Size

position = item.Position.Value;

double vEnd = position.V + currentMeasure.V;
var vEnd = position.V + currentMeasure.V;
if (vEnd < realizationBounds.VMin)
{
// Item is "above" the bounds
if (item.Element != null)
if (item.Element is not null)
{
context.RecycleElement(item.Element);
item.Element = null;
}

continue;
}
else if (position.V > realizationBounds.VMax)
{
// Item is "below" the bounds.
if (item.Element != null)
if (item.Element is not null)
{
context.RecycleElement(item.Element);
item.Element = null;
Expand All @@ -214,14 +208,14 @@ protected override Size MeasureOverride(VirtualizingLayoutContext context, Size
// We don't need to measure anything below the bounds
break;
}
else if (measured == false)
else if (!measured)
{
// 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)
currentMeasure = new UvMeasure(Orientation, item.Element.DesiredSize);
if (currentMeasure != item.Measure)
{
// this item changed size; we need to recalculate layout for everything after this
state.RemoveFromIndex(i + 1);
Expand Down Expand Up @@ -249,70 +243,43 @@ protected override Size MeasureOverride(VirtualizingLayoutContext context, Size
// 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;

// Propagating an infinite size causes a crash. This can happen if the parent is scrollable and infinite in the opposite
// axis to the panel. Clearing to zero prevents the crash.
// This is likely an incorrect use of the control by the developer, however we need stability here so setting a default that wont crash.
if (double.IsInfinity(totalMeasure.U))
{
totalMeasure.U = 0.0;
}
// This is likely an incorrect use of the control by the developer, however we need stability here so setting a default that won't crash.

totalMeasure.V = state.GetHeight();
var totalMeasure = new UvMeasure
{
U = double.IsInfinity(parentMeasure.U) ? 0 : Math.Ceiling(parentMeasure.U),
V = state.GetHeight()
};

totalMeasure.U = Math.Ceiling(totalMeasure.U);
return Orientation == Orientation.Horizontal ? new Size((float)totalMeasure.U, (float)totalMeasure.V) : new Size((float)totalMeasure.V, (float)totalMeasure.U);
return totalMeasure.GetSize(Orientation);
}

/// <inheritdoc />
protected 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)
bool ArrangeItem(WrapItem item)
{
if (item.Measure.HasValue == false)
{
return false;
}

if (item.Position == null)
if (item is { Measure: null } or { 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;
var 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))
if (realizationBounds.VMin <= position.V + desiredMeasure.V && position.V <= realizationBounds.VMax)
{
// place the item
UIElement child = context.GetOrCreateElementAt(item.Index);
if (Orientation == Orientation.Horizontal)
{
child.Arrange(new Rect((float)position.U, (float)position.V, (float)desiredMeasure.U, (float)desiredMeasure.V));
}
else
{
child.Arrange(new Rect((float)position.V, (float)position.U, (float)desiredMeasure.V, (float)desiredMeasure.U));
}
var child = context.GetOrCreateElementAt(item.Index);
child.Arrange(new Rect(position.GetPoint(Orientation), desiredMeasure.GetSize(Orientation)));
}
else if (position.V > realizationBounds.VMax)
{
Expand All @@ -322,10 +289,9 @@ bool Arrange(WrapItem item, bool isLast = false)
return true;
}

for (var i = 0; i < context.ItemCount; i++)
for (var i = 0; i < context.ItemCount; ++i)
{
bool continueArranging = Arrange(state.GetItemAt(i));
if (continueArranging == false)
if (!ArrangeItem(state.GetItemAt(i)))
{
break;
}
Expand Down
Loading